一大波改动
这个提交包含在:
父节点
1552bfb561
当前提交
a917932a2d
9
package-lock.json
自动生成的
9
package-lock.json
自动生成的
@ -1349,6 +1349,14 @@
|
||||
"resolved": "docs-site",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@xuqm/vue3-sdk": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://nexus.xuqinmin.com/repository/npm-hosted/@xuqm/vue3-sdk/-/vue3-sdk-0.2.2.tgz",
|
||||
"integrity": "sha512-1fZrqEcPHf7E7LLIx2QhWG8s/yWxmNa2QNyvhvmsvyp2/LkO0uX++l/4aHZqvfiqlUDhcr892I4YdRoYuzAcQw==",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
||||
@ -4718,6 +4726,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@xuqm/vue3-sdk": "^0.2.2",
|
||||
"axios": "^1.7.9",
|
||||
"element-plus": "^2.9.1",
|
||||
"pinia": "^3.0.1",
|
||||
|
||||
@ -9,10 +9,10 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"@xuqm/vue3-sdk": "0.2.0",
|
||||
"element-plus": "^2.9.1",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@xuqm/vue3-sdk": "^0.2.2",
|
||||
"axios": "^1.7.9",
|
||||
"element-plus": "^2.9.1",
|
||||
"pinia": "^3.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
@ -20,9 +20,9 @@
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.2",
|
||||
"vue-tsc": "^2.2.8",
|
||||
"unplugin-auto-import": "^0.18.2",
|
||||
"unplugin-vue-components": "^0.27.4"
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^6.2.2",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { init, login, ImClient, type ImMessage } from '@xuqm/vue3-sdk'
|
||||
import { ImClient, type ImMessage } from '@xuqm/vue3-sdk'
|
||||
import client from '@/api/client'
|
||||
|
||||
export interface StoreReviewRefreshEvent {
|
||||
@ -24,10 +24,6 @@ interface PlatformEventTokenResponse {
|
||||
let imClient: ImClient | null = null
|
||||
let activeAppKey = ''
|
||||
|
||||
function sdkBaseUrl() {
|
||||
return import.meta.env.VITE_IM_API_BASE_URL ?? ''
|
||||
}
|
||||
|
||||
function sdkWsUrl() {
|
||||
return import.meta.env.VITE_IM_WS_URL ?? ''
|
||||
}
|
||||
@ -65,22 +61,18 @@ export async function connectStoreReviewRealtime(appKey: string, onEvent: (event
|
||||
const res = await client.get<{ data: PlatformEventTokenResponse }>('/im/platform-events/token', {
|
||||
params: { appKey },
|
||||
})
|
||||
const token = res.data.data ?? (res.data as unknown as PlatformEventTokenResponse)
|
||||
init({
|
||||
appKey,
|
||||
baseUrl: sdkBaseUrl(),
|
||||
wsUrl: sdkWsUrl(),
|
||||
debug: import.meta.env.DEV,
|
||||
})
|
||||
login(token.userId, token.token)
|
||||
const tokenData = res.data.data ?? (res.data as unknown as PlatformEventTokenResponse)
|
||||
const platformToken = tokenData.token
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.debug('[tenant-platform][IM] store review realtime connected token acquired', {
|
||||
console.debug('[tenant-platform][IM] store review realtime token acquired', {
|
||||
appKey,
|
||||
userId: token.userId,
|
||||
userId: tokenData.userId,
|
||||
})
|
||||
}
|
||||
|
||||
const clientInstance = new ImClient()
|
||||
const wsUrl = sdkWsUrl() || undefined
|
||||
const clientInstance = new ImClient({ tokenSupplier: () => platformToken, wsUrl })
|
||||
clientInstance.on('message', (message) => {
|
||||
const event = parseEvent(message)
|
||||
if (!event || event.appKey !== activeAppKey) return
|
||||
|
||||
@ -495,73 +495,80 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- Store Review Detail Dialog -->
|
||||
<el-dialog v-model="showStoreReviewDetail" title="应用商店状态详情" :width="dialogWidth">
|
||||
<el-dialog v-model="showStoreReviewDetail" title="应用商店状态详情" :width="isMobile ? '100%' : '760px'">
|
||||
<div v-if="storeReviewDetailVersion">
|
||||
<el-descriptions :column="1" border style="margin-bottom:16px">
|
||||
<el-descriptions-item label="版本">
|
||||
{{ storeReviewDetailVersion.versionName }} · {{ storeReviewDetailVersion.versionCode }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发布状态">
|
||||
<!-- Version summary bar -->
|
||||
<div class="review-detail-header">
|
||||
<span class="review-detail-version">
|
||||
{{ storeReviewDetailVersion.versionName }}
|
||||
<span class="review-detail-code">· {{ storeReviewDetailVersion.versionCode }}</span>
|
||||
</span>
|
||||
<el-tag :type="statusTagType(storeReviewDetailVersion)" size="small">
|
||||
{{ statusLabel(storeReviewDetailVersion) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="市场提交目标">
|
||||
<span v-if="parseStoreTargets(storeReviewDetailVersion.storeSubmitTargets).length">
|
||||
{{ parseStoreTargets(storeReviewDetailVersion.storeSubmitTargets).map(storeLabel).join('、') }}
|
||||
<span v-if="storeReviewDetailLive" class="review-live-dot" title="实时更新中">
|
||||
<span class="live-pulse" />实时
|
||||
</span>
|
||||
<span v-else class="text-muted">未配置</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上传时间">
|
||||
{{ formatTime(storeReviewDetailVersion.createdAt) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<el-table :data="storeReviewDetailItems" border stripe>
|
||||
<el-table-column prop="store" label="市场" width="150">
|
||||
<template #default="{row}">{{ storeLabel(row.store) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="state" label="状态" width="120">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="reviewTagType(row.state)" size="small">
|
||||
{{ reviewLabel(row.state) }}
|
||||
<!-- Per-store cards -->
|
||||
<div class="store-review-cards">
|
||||
<div
|
||||
v-for="item in storeReviewDetailItems"
|
||||
:key="item.store"
|
||||
class="review-store-card"
|
||||
:class="[`review-card--${item.state.toLowerCase()}`, { 'review-card--active': isActiveState(item.state) }]"
|
||||
>
|
||||
<!-- Card header: store name + status -->
|
||||
<div class="review-card-head">
|
||||
<span class="review-card-name">{{ storeLabel(item.store) }}</span>
|
||||
<el-tag :type="reviewTagType(item.state)" size="small" class="review-card-tag">
|
||||
{{ reviewLabel(item.state) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stage" label="阶段" width="120">
|
||||
<template #default="{row}">
|
||||
<el-tag v-if="row.stage" type="info" size="small">{{ row.stage }}</el-tag>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="submittedAt" label="提交时间" width="180">
|
||||
<template #default="{row}">
|
||||
<span>{{ row.submittedAt ? formatTime(row.submittedAt) : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="batchId" label="批次 ID" width="220" show-overflow-tooltip>
|
||||
<template #default="{row}">
|
||||
<span>{{ row.batchId || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="reason" label="失败原因 / 说明" min-width="260" show-overflow-tooltip>
|
||||
<template #default="{row}">
|
||||
<el-text v-if="row.reason" :type="row.state === 'REJECTED' ? 'danger' : 'default'">
|
||||
{{ row.reason }}
|
||||
</el-text>
|
||||
<span v-else class="text-muted">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="storeReviewDetailItems.some(item => item.state === 'REJECTED')"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-top:16px"
|
||||
title="存在审核失败的市场,请查看上方失败原因。"
|
||||
/>
|
||||
<!-- Stage progress -->
|
||||
<div class="review-card-progress">
|
||||
<div
|
||||
v-for="step in reviewSteps"
|
||||
:key="step.stage"
|
||||
class="review-step"
|
||||
:class="reviewStepClass(item.state, item.stage, step)"
|
||||
>
|
||||
<div class="review-step-dot">
|
||||
<el-icon v-if="reviewStepDone(item.state, step)"><CircleCheckFilled /></el-icon>
|
||||
<el-icon v-else-if="reviewStepActive(item.state, item.stage, step)" class="is-loading"><Loading /></el-icon>
|
||||
<span v-else class="step-dot-empty" />
|
||||
</div>
|
||||
<span class="review-step-label">{{ step.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="review-card-meta">
|
||||
<div v-if="item.submittedAt" class="review-card-meta-item">
|
||||
<span class="meta-label">提交时间</span>
|
||||
<span class="meta-value">{{ formatTime(item.submittedAt) }}</span>
|
||||
</div>
|
||||
<div v-if="item.updatedAt" class="review-card-meta-item">
|
||||
<span class="meta-label">更新时间</span>
|
||||
<span class="meta-value">{{ formatTime(item.updatedAt) }}</span>
|
||||
</div>
|
||||
<div v-if="item.batchId" class="review-card-meta-item">
|
||||
<span class="meta-label">批次</span>
|
||||
<span class="meta-value meta-mono">{{ item.batchId.slice(0, 8) }}…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rejection / withdrawal reason -->
|
||||
<div v-if="item.reason && (item.state === 'REJECTED' || item.state === 'WITHDRAWN')" class="review-card-reason">
|
||||
<el-icon><WarningFilled /></el-icon>
|
||||
<span>{{ item.reason }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!storeReviewDetailItems.length" description="暂无商店状态数据" />
|
||||
</div>
|
||||
<el-empty v-else description="暂无商店状态数据" />
|
||||
<template #footer>
|
||||
@ -762,7 +769,7 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { CircleCheckFilled, Loading, UploadFilled, WarningFilled } from '@element-plus/icons-vue'
|
||||
import { appApi, type App } from '@/api/app'
|
||||
import { fileApi } from '@/api/file'
|
||||
import {
|
||||
@ -1290,6 +1297,7 @@ const submitStoreScheduledAt = ref('')
|
||||
const showStoreReviewDetail = ref(false)
|
||||
const storeReviewDetailVersion = ref<AppVersion | null>(null)
|
||||
const storeReviewDetailItems = ref<{ store: string; state: string; reason?: string; stage?: string; submittedAt?: string; updatedAt?: string; batchId?: string }[]>([])
|
||||
const storeReviewDetailLive = ref(false)
|
||||
|
||||
function openSubmitStoreDialog(row: AppVersion) {
|
||||
submitStoreVersion.value = row
|
||||
@ -1312,6 +1320,7 @@ function parseStoreTargets(json?: string) {
|
||||
function openStoreReviewDetail(row: AppVersion) {
|
||||
storeReviewDetailVersion.value = row
|
||||
storeReviewDetailItems.value = parseStoreReview(row.storeReviewStatus)
|
||||
storeReviewDetailLive.value = false
|
||||
showStoreReviewDetail.value = true
|
||||
}
|
||||
|
||||
@ -1833,11 +1842,66 @@ function storeLabel(type: string) {
|
||||
}
|
||||
|
||||
function reviewLabel(state: string): string {
|
||||
return { PENDING: '待提交', SUBMITTING: '提交中', UNDER_REVIEW: '审核中', APPROVED: '已通过', REJECTED: '已拒绝' }[state] ?? state
|
||||
return {
|
||||
PENDING: '待提交', SUBMITTING: '提交中', UNDER_REVIEW: '审核中',
|
||||
APPROVED: '已通过', REJECTED: '已拒绝', WITHDRAWN: '已撤回',
|
||||
}[state] ?? state
|
||||
}
|
||||
|
||||
function reviewTagType(state: string): string {
|
||||
return { PENDING: 'info', SUBMITTING: 'primary', UNDER_REVIEW: 'warning', APPROVED: 'success', REJECTED: 'danger' }[state] ?? ''
|
||||
return {
|
||||
PENDING: 'info', SUBMITTING: 'primary', UNDER_REVIEW: 'warning',
|
||||
APPROVED: 'success', REJECTED: 'danger', WITHDRAWN: '',
|
||||
}[state] ?? ''
|
||||
}
|
||||
|
||||
function isActiveState(state: string) {
|
||||
return state === 'PENDING' || state === 'SUBMITTING' || state === 'UNDER_REVIEW'
|
||||
}
|
||||
|
||||
// Step definitions for per-store progress
|
||||
const reviewSteps = [
|
||||
{ stage: 'QUEUED', label: '排队' },
|
||||
{ stage: 'SUBMITTING', label: '上传' },
|
||||
{ stage: 'SUBMITTED', label: '审核中' },
|
||||
{ stage: 'DONE', label: '完成' },
|
||||
]
|
||||
|
||||
const STAGE_ORDER: Record<string, number> = {
|
||||
QUEUED: 0, SUBMITTING: 1, SUBMITTED: 2,
|
||||
APPROVED: 3, FAILED: 3, WITHDRAWN: 3, DONE: 3,
|
||||
}
|
||||
|
||||
function stageIndex(state: string, stage: string): number {
|
||||
if (state === 'APPROVED' || state === 'REJECTED' || state === 'WITHDRAWN') return 3
|
||||
if (stage) return STAGE_ORDER[stage] ?? 0
|
||||
return { PENDING: 0, SUBMITTING: 1, UNDER_REVIEW: 2 }[state] ?? 0
|
||||
}
|
||||
|
||||
function reviewStepDone(state: string, step: { stage: string }): boolean {
|
||||
if (step.stage === 'DONE') return state === 'APPROVED'
|
||||
const si = stageIndex(state, '')
|
||||
const ti = STAGE_ORDER[step.stage] ?? 0
|
||||
if (state === 'APPROVED') return true
|
||||
if (state === 'REJECTED') return ti < 3
|
||||
if (state === 'WITHDRAWN') return false
|
||||
return ti < si
|
||||
}
|
||||
|
||||
function reviewStepActive(state: string, stage: string, step: { stage: string }): boolean {
|
||||
if (step.stage === 'DONE') return false
|
||||
const cur = stageIndex(state, stage)
|
||||
const ti = STAGE_ORDER[step.stage] ?? 0
|
||||
return ti === cur && isActiveState(state)
|
||||
}
|
||||
|
||||
function reviewStepClass(state: string, stage: string, step: { stage: string }): Record<string, boolean> {
|
||||
return {
|
||||
'step--done': reviewStepDone(state, step),
|
||||
'step--active': reviewStepActive(state, stage, step),
|
||||
'step--failed': step.stage === 'DONE' && state === 'REJECTED',
|
||||
'step--withdrawn': step.stage === 'DONE' && state === 'WITHDRAWN',
|
||||
}
|
||||
}
|
||||
|
||||
function operationResourceLabel(resourceType: string) {
|
||||
@ -1981,14 +2045,43 @@ onMounted(() => {
|
||||
loadPublishConfig()
|
||||
loadOperationLogs()
|
||||
void connectStoreReviewRealtime(appKey, (event: StoreReviewRefreshEvent) => {
|
||||
notifyStoreReviewRefresh(event.appKey)
|
||||
const storeName = storeLabel(event.storeType || '') || event.storeType || '应用市场'
|
||||
const stateName = reviewLabel((event.reviewState || '').toUpperCase()) || event.reviewState || '状态变更'
|
||||
ElMessage.success(
|
||||
event.reviewReason
|
||||
? `${storeName} 审核状态更新为 ${stateName}:${event.reviewReason}`
|
||||
: `${storeName} 审核状态更新为 ${stateName}`,
|
||||
)
|
||||
|
||||
// Patch the open dialog card directly for real-time feel
|
||||
const openVersionId = storeReviewDetailVersion.value?.id
|
||||
if (showStoreReviewDetail.value && openVersionId && openVersionId === event.versionId && event.storeType) {
|
||||
storeReviewDetailLive.value = true
|
||||
const idx = storeReviewDetailItems.value.findIndex(i => i.store === event.storeType)
|
||||
const patch = {
|
||||
store: event.storeType,
|
||||
state: (event.reviewState || '').toUpperCase(),
|
||||
stage: event.stage || '',
|
||||
reason: event.reviewReason || '',
|
||||
batchId: event.batchId || storeReviewDetailItems.value[idx]?.batchId || '',
|
||||
submittedAt: storeReviewDetailItems.value[idx]?.submittedAt || new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
if (idx >= 0) {
|
||||
storeReviewDetailItems.value[idx] = patch
|
||||
} else {
|
||||
storeReviewDetailItems.value.push(patch)
|
||||
}
|
||||
}
|
||||
|
||||
// Only show toast for non-trivial state transitions
|
||||
const state = (event.reviewState || '').toUpperCase()
|
||||
if (state && state !== 'PENDING') {
|
||||
ElMessage({
|
||||
message: event.reviewReason
|
||||
? `${storeName} → ${stateName}:${event.reviewReason}`
|
||||
: `${storeName} → ${stateName}`,
|
||||
type: state === 'REJECTED' || state === 'WITHDRAWN' ? 'error'
|
||||
: state === 'APPROVED' ? 'success' : 'info',
|
||||
duration: 4000,
|
||||
})
|
||||
}
|
||||
|
||||
scheduleStoreReviewReload()
|
||||
}).catch((error) => {
|
||||
if (import.meta.env.DEV) {
|
||||
@ -2311,4 +2404,229 @@ onBeforeUnmount(() => {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
/* ── Store review card dialog ─────────────────────────────────── */
|
||||
.review-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 0 0 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.review-detail-version {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.review-detail-code {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.review-live-dot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-success);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.live-pulse {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--el-color-success);
|
||||
animation: live-blink 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes live-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.2; }
|
||||
}
|
||||
|
||||
.store-review-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.review-store-card {
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
background: var(--el-bg-color);
|
||||
transition: box-shadow 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.review-store-card.review-card--active {
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
box-shadow: 0 0 0 2px var(--el-color-primary-light-8);
|
||||
}
|
||||
|
||||
.review-store-card.review-card--approved {
|
||||
border-color: var(--el-color-success-light-5);
|
||||
}
|
||||
|
||||
.review-store-card.review-card--rejected,
|
||||
.review-store-card.review-card--withdrawn {
|
||||
border-color: var(--el-border-color-light);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.review-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.review-card-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.review-card-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Progress steps */
|
||||
.review-card-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.review-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.review-step:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--el-border-color);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.review-step.step--done:not(:last-child)::after {
|
||||
background: var(--el-color-success);
|
||||
}
|
||||
|
||||
.review-step.step--active:not(:last-child)::after {
|
||||
background: var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.review-step-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.step-dot-empty {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--el-border-color);
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.review-step.step--done .review-step-dot {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.review-step.step--active .review-step-dot {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.review-step.step--failed .review-step-dot {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.review-step.step--withdrawn .review-step-dot {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.review-step-label {
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.review-step.step--done .review-step-label {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.review-step.step--active .review-step-label {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.review-step.step--failed .review-step-label {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
/* Meta rows */
|
||||
.review-card-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.review-card-meta-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: var(--el-text-color-placeholder);
|
||||
flex-shrink: 0;
|
||||
min-width: 38px;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: var(--el-text-color-regular);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.meta-mono {
|
||||
font-family: monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Rejection reason */
|
||||
.review-card-reason {
|
||||
font-size: 11px;
|
||||
color: var(--el-color-danger);
|
||||
background: var(--el-color-danger-light-9);
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
margin-top: 6px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户