style: format web apps with prettier and sort imports

Apply repo-wide Prettier/lint normalization across admin-web,
desktop-client and ops-web: single quotes, no semicolons, trailing
commas, consistent line wrapping, and import ordering. Also drop an
unused brand-logo import in DesktopShell.vue.

No behavior changes — formatting only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 11:56:18 +08:00
parent a44ed21967
commit aa96143754
81 changed files with 2929 additions and 2252 deletions
@@ -19,8 +19,8 @@
<script setup lang="ts">
import type { RadioChangeEvent } from 'ant-design-vue'
import { publishPlatformOptions } from '@/lib/compliance-display'
import type { CompliancePlatformScope } from '@/lib/compliance'
import { publishPlatformOptions } from '@/lib/compliance-display'
defineProps<{
scope: CompliancePlatformScope
+1 -6
View File
@@ -36,12 +36,7 @@ const CHUNK_ERROR_PATTERNS = [
]
export function isChunkLoadError(err: unknown): boolean {
const msg =
err instanceof Error
? err.message
: typeof err === 'string'
? err
: ''
const msg = err instanceof Error ? err.message : typeof err === 'string' ? err : ''
return CHUNK_ERROR_PATTERNS.some((re) => re.test(msg))
}
+2 -1
View File
@@ -21,7 +21,8 @@ export async function copyTextToClipboard(text: string): Promise<void> {
function copyTextWithSelection(text: string): boolean {
const textarea = document.createElement('textarea')
const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : null
const activeElement =
document.activeElement instanceof HTMLElement ? document.activeElement : null
textarea.value = text
textarea.setAttribute('readonly', '')
+3 -1
View File
@@ -127,7 +127,9 @@ export const complianceApi = {
)
},
deleteTerm(dictionaryId: number, termId: number) {
return http.delete<{ deleted: boolean }>(`/compliance/dictionaries/${dictionaryId}/terms/${termId}`)
return http.delete<{ deleted: boolean }>(
`/compliance/dictionaries/${dictionaryId}/terms/${termId}`,
)
},
publishDictionary(id: number) {
return http.post<OpsComplianceDictionary>(`/compliance/dictionaries/${id}/publish`)
+22 -11
View File
@@ -209,16 +209,19 @@ async function uploadOSSInChunks(
onProgress?: (progress: DesktopClientReleaseUploadProgress) => void
},
): Promise<DesktopClientReleaseUploadResult> {
const session = await http.post<DesktopClientReleaseUploadSession>('/desktop-client/release-uploads', {
platform: target.platform,
arch: target.arch,
channel: target.channel,
version: target.version,
kind: target.kind ?? 'installer',
file_name: file.name,
file_size_bytes: file.size,
chunk_size_bytes: DESKTOP_CLIENT_RELEASE_CHUNK_SIZE_BYTES,
})
const session = await http.post<DesktopClientReleaseUploadSession>(
'/desktop-client/release-uploads',
{
platform: target.platform,
arch: target.arch,
channel: target.channel,
version: target.version,
kind: target.kind ?? 'installer',
file_name: file.name,
file_size_bytes: file.size,
chunk_size_bytes: DESKTOP_CLIENT_RELEASE_CHUNK_SIZE_BYTES,
},
)
const chunkSize = session.chunk_size_bytes || DESKTOP_CLIENT_RELEASE_CHUNK_SIZE_BYTES
const chunkCount = session.chunk_count || Math.ceil(file.size / chunkSize)
@@ -232,7 +235,15 @@ async function uploadOSSInChunks(
try {
for (let index = 0; index < chunkCount; index += 1) {
if (uploaded.has(index)) continue
await uploadChunkWithRetry(file, session.upload_id, index, chunkSize, chunkCount, confirmedBytes, target)
await uploadChunkWithRetry(
file,
session.upload_id,
index,
chunkSize,
chunkCount,
confirmedBytes,
target,
)
confirmedBytes += chunkByteSize(file.size, chunkSize, index, chunkCount)
emitChunkProgress(target, confirmedBytes, file.size, chunkCount, index)
}
+18 -4
View File
@@ -77,8 +77,14 @@ export const opsMediaSupplyApi = {
params as Record<string, unknown>,
)
},
setPrice(id: number, payload: { price_type?: string; sell_price_cents: number; enabled?: boolean }) {
return http.put<{ updated: boolean }, typeof payload>(`/media-supply/resources/${id}/price`, payload)
setPrice(
id: number,
payload: { price_type?: string; sell_price_cents: number; enabled?: boolean },
) {
return http.put<{ updated: boolean }, typeof payload>(
`/media-supply/resources/${id}/price`,
payload,
)
},
setVisibility(id: number, customerVisible: boolean) {
return http.put<{ updated: boolean }, { customer_visible: boolean }>(
@@ -101,7 +107,15 @@ export const opsMediaSupplyApi = {
params as Record<string, unknown>,
)
},
adjustWallet(payload: { tenant_id: number; user_id: number; delta_cents: number; note?: string }) {
return http.post<OpsMediaSupplyWallet, typeof payload>('/media-supply/wallets/adjustments', payload)
adjustWallet(payload: {
tenant_id: number
user_id: number
delta_cents: number
note?: string
}) {
return http.post<OpsMediaSupplyWallet, typeof payload>(
'/media-supply/wallets/adjustments',
payload,
)
},
}
+6 -3
View File
@@ -95,9 +95,12 @@ export const objectStorageApi = {
)
},
createFolder(prefix: string) {
return http.post<ObjectStorageFolderCreateResult, { prefix: string }>('/object-storage/folders', {
prefix,
})
return http.post<ObjectStorageFolderCreateResult, { prefix: string }>(
'/object-storage/folders',
{
prefix,
},
)
},
deleteFolder(prefix: string) {
return http.delete<ObjectStorageFolderDeleteResult>(
+6 -1
View File
@@ -81,7 +81,12 @@ export const useAuthStore = defineStore('ops-auth', () => {
async function buildSecureLoginPayload(
username: string,
password: string,
): Promise<{ username: string; password?: string; encrypted_password?: string; password_key_id?: string }> {
): Promise<{
username: string
password?: string
encrypted_password?: string
password_key_id?: string
}> {
if (!browserSupportsPasswordCipher()) {
return { username, password }
}
+3 -1
View File
@@ -641,7 +641,9 @@ async function resetLoginLock(row: AdminUserRow) {
} else if (result.fallback_deleted_keys > 0) {
message.success(`登录锁定已清除,已清理 ${result.fallback_deleted_keys} 个本机保护状态`)
} else {
message.warning('未发现登录锁 Redis key,请检查 ops-api 与 admin-web 使用的 Redis 地址和 DB 是否一致')
message.warning(
'未发现登录锁 Redis key,请检查 ops-api 与 admin-web 使用的 Redis 地址和 DB 是否一致',
)
}
} catch (error) {
showOpsError(error)
@@ -283,18 +283,16 @@
/>
</a-form-item>
</template>
<a-form-item v-else-if="form.updater_download_source === 'custom'" label="更新包自定义地址">
<a-form-item
v-else-if="form.updater_download_source === 'custom'"
label="更新包自定义地址"
>
<a-input
v-model:value="form.updater_custom_download_url"
placeholder="https://download.example.com/desktop/app.zip"
/>
</a-form-item>
<a-alert
v-else
type="info"
show-icon
message="复用官网安装包"
/>
<a-alert v-else type="info" show-icon message="复用官网安装包" />
<div v-if="form.updater_download_source !== ''" class="form-grid">
<a-form-item label="更新包文件名">
@@ -347,6 +345,7 @@ import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { copyTextToClipboard } from '@/lib/clipboard'
import {
archOptions,
desktopClientReleaseApi,
@@ -357,7 +356,6 @@ import {
type DesktopReleasePlatform,
type DesktopReleaseSource,
} from '@/lib/desktop-client-releases'
import { copyTextToClipboard } from '@/lib/clipboard'
import { showOpsError } from '@/lib/errors'
const enabledOptions = [
@@ -620,7 +618,8 @@ async function beforeUploadReleasePackage(file: File, kind: 'installer' | 'updat
function uploadButtonText(kind: 'installer' | 'updater') {
const isUploading = kind === 'installer' ? uploading.value : updaterUploading.value
const progress = kind === 'installer' ? uploadProgress.value : updaterUploadProgress.value
const hasUploaded = kind === 'installer' ? Boolean(form.oss_object_key) : Boolean(form.updater_oss_object_key)
const hasUploaded =
kind === 'installer' ? Boolean(form.oss_object_key) : Boolean(form.updater_oss_object_key)
if (isUploading && typeof progress === 'number' && progress < 100) {
return `上传中 ${progress}%`
}
@@ -749,10 +748,7 @@ function updaterSourceText(row: DesktopClientRelease): string {
return row.updater_download_source === 'oss' ? 'OSS' : '自定义'
}
function assetTargetText(
row: DesktopClientRelease,
kind: 'installer' | 'updater',
): string {
function assetTargetText(row: DesktopClientRelease, kind: 'installer' | 'updater'): string {
if (kind === 'installer' || !row.updater_download_source) {
return row.download_source === 'oss'
? (row.oss_object_key ?? '未配置')
+85 -22
View File
@@ -43,9 +43,26 @@
placeholder="阶段"
@change="resetAndReload"
/>
<a-input v-model:value="filter.kind" placeholder="类型" style="width: 160px" allow-clear @press-enter="resetAndReload" />
<a-input v-model:value="filter.tenantId" placeholder="租户 ID" style="width: 120px" allow-clear @press-enter="resetAndReload" />
<a-range-picker v-model:value="dateRange" show-time style="width: 360px" @change="resetAndReload" />
<a-input
v-model:value="filter.kind"
placeholder="类型"
style="width: 160px"
allow-clear
@press-enter="resetAndReload"
/>
<a-input
v-model:value="filter.tenantId"
placeholder="租户 ID"
style="width: 120px"
allow-clear
@press-enter="resetAndReload"
/>
<a-range-picker
v-model:value="dateRange"
show-time
style="width: 360px"
@change="resetAndReload"
/>
<a-button @click="resetAndReload">查询</a-button>
</div>
@@ -78,7 +95,9 @@
</template>
<template v-else-if="column.key === 'tenant'">
<div>{{ record.tenant_name || '—' }}</div>
<div class="muted">T{{ record.tenant_id || '—' }} / W{{ record.workspace_id || '—' }}</div>
<div class="muted">
T{{ record.tenant_id || '—' }} / W{{ record.workspace_id || '—' }}
</div>
</template>
<template v-else-if="column.key === 'worker'">
<div class="mono-line">{{ record.worker || record.queue || '—' }}</div>
@@ -99,15 +118,33 @@
</a-button>
</a-tooltip>
<a-tooltip title="重试">
<a-popconfirm title="确认重试该任务?" ok-text="重试" cancel-text="取消" @confirm="retryJob(record)">
<a-button type="text" class="action-btn action-play" :disabled="!record.can_retry">
<a-popconfirm
title="确认重试该任务?"
ok-text="重试"
cancel-text="取消"
@confirm="retryJob(record)"
>
<a-button
type="text"
class="action-btn action-play"
:disabled="!record.can_retry"
>
<ReloadOutlined />
</a-button>
</a-popconfirm>
</a-tooltip>
<a-tooltip title="取消">
<a-popconfirm title="确认取消该任务?" ok-text="取消任务" cancel-text="关闭" @confirm="cancelJob(record)">
<a-button type="text" class="action-btn action-delete" :disabled="!record.can_cancel">
<a-popconfirm
title="确认取消该任务?"
ok-text="取消任务"
cancel-text="关闭"
@confirm="cancelJob(record)"
>
<a-button
type="text"
class="action-btn action-delete"
:disabled="!record.can_cancel"
>
<StopOutlined />
</a-button>
</a-popconfirm>
@@ -122,24 +159,43 @@
<a-spin :spinning="detailLoading">
<div v-if="selectedDetail" class="detail-stack">
<a-descriptions :column="2" size="small" bordered>
<a-descriptions-item label="来源">{{ sourceLabel(selectedDetail.source) }}</a-descriptions-item>
<a-descriptions-item label="来源">
{{ sourceLabel(selectedDetail.source) }}
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="phaseColor(selectedDetail.phase)">{{ phaseLabel(selectedDetail.phase) }}</a-tag>
<a-tag :color="phaseColor(selectedDetail.phase)">
{{ phaseLabel(selectedDetail.phase) }}
</a-tag>
{{ selectedDetail.status }}
</a-descriptions-item>
<a-descriptions-item label="任务 ID">
<code>{{ selectedDetail.id }}</code>
</a-descriptions-item>
<a-descriptions-item label="类型">{{ selectedDetail.kind }}</a-descriptions-item>
<a-descriptions-item label="租户">{{ selectedDetail.tenant_name || selectedDetail.tenant_id || '—' }}</a-descriptions-item>
<a-descriptions-item label="Workspace">{{ selectedDetail.workspace_name || selectedDetail.workspace_id || '—' }}</a-descriptions-item>
<a-descriptions-item label="队列">{{ selectedDetail.queue || '—' }}</a-descriptions-item>
<a-descriptions-item label="Worker">{{ selectedDetail.worker || '—' }}</a-descriptions-item>
<a-descriptions-item label="创建">{{ formatDate(selectedDetail.created_at) }}</a-descriptions-item>
<a-descriptions-item label="更新">{{ formatDate(selectedDetail.updated_at) }}</a-descriptions-item>
<a-descriptions-item label="租户">
{{ selectedDetail.tenant_name || selectedDetail.tenant_id || '—' }}
</a-descriptions-item>
<a-descriptions-item label="Workspace">
{{ selectedDetail.workspace_name || selectedDetail.workspace_id || '—' }}
</a-descriptions-item>
<a-descriptions-item label="队列">
{{ selectedDetail.queue || '—' }}
</a-descriptions-item>
<a-descriptions-item label="Worker">
{{ selectedDetail.worker || '—' }}
</a-descriptions-item>
<a-descriptions-item label="创建">
{{ formatDate(selectedDetail.created_at) }}
</a-descriptions-item>
<a-descriptions-item label="更新">
{{ formatDate(selectedDetail.updated_at) }}
</a-descriptions-item>
</a-descriptions>
<section v-if="selectedDetail.error_message || selectedDetail.error" class="detail-section">
<section
v-if="selectedDetail.error_message || selectedDetail.error"
class="detail-section"
>
<h3>错误</h3>
<pre>{{ formatJSON(selectedDetail.error || selectedDetail.error_message) }}</pre>
</section>
@@ -342,7 +398,9 @@ async function openDetail(row: JobSummary) {
detailLoading.value = true
selectedDetail.value = null
try {
selectedDetail.value = await http.get<JobDetail>(`/jobs/${row.source}/${encodeURIComponent(row.id)}`)
selectedDetail.value = await http.get<JobDetail>(
`/jobs/${row.source}/${encodeURIComponent(row.id)}`,
)
} catch (error) {
showError(error)
} finally {
@@ -352,7 +410,9 @@ async function openDetail(row: JobSummary) {
async function retryJob(row: JobSummary) {
try {
const item = await http.post<JobDetail>(`/jobs/${row.source}/${encodeURIComponent(row.id)}/retry`)
const item = await http.post<JobDetail>(
`/jobs/${row.source}/${encodeURIComponent(row.id)}/retry`,
)
message.success('已提交重试')
patchRow(item)
if (selectedDetail.value?.uid === item.uid) selectedDetail.value = item
@@ -363,9 +423,12 @@ async function retryJob(row: JobSummary) {
async function cancelJob(row: JobSummary) {
try {
const item = await http.post<JobDetail>(`/jobs/${row.source}/${encodeURIComponent(row.id)}/cancel`, {
reason: 'ops console cancel',
})
const item = await http.post<JobDetail>(
`/jobs/${row.source}/${encodeURIComponent(row.id)}/cancel`,
{
reason: 'ops console cancel',
},
)
message.success('已取消任务')
patchRow(item)
if (selectedDetail.value?.uid === item.uid) selectedDetail.value = item
+10 -2
View File
@@ -7,7 +7,11 @@
}"
>
<div class="brand">
<img class="brand-logo brand-logo--white" src="/logos/icon-with-title.svg" alt="省心推运营控制台" />
<img
class="brand-logo brand-logo--white"
src="/logos/icon-with-title.svg"
alt="省心推运营控制台"
/>
<span class="brand-badge brand-badge--white">运营端</span>
</div>
<div class="characters-area">
@@ -29,7 +33,11 @@
<div class="right">
<div class="form-wrapper">
<div class="mobile-brand">
<img class="brand-logo brand-logo--mobile" src="/logos/icon-with-title.svg" alt="省心推运营控制台" />
<img
class="brand-logo brand-logo--mobile"
src="/logos/icon-with-title.svg"
alt="省心推运营控制台"
/>
<span class="brand-badge">运营端</span>
</div>
<div class="header">
+43 -12
View File
@@ -93,7 +93,9 @@
<strong v-else>{{ record.name }}</strong>
<div class="resource-sub">
<code>{{ record.supplier_resource_id }}</code>
<a-tag :color="statusColor(record.status)">{{ statusLabel(record.status) }}</a-tag>
<a-tag :color="statusColor(record.status)">
{{ statusLabel(record.status) }}
</a-tag>
</div>
</div>
</template>
@@ -104,7 +106,11 @@
</div>
</template>
<template v-else-if="column.key === 'margin'">
<span :class="record.sell_price_cents <= record.cost_price_cents ? 'margin-risk' : 'margin-ok'">
<span
:class="
record.sell_price_cents <= record.cost_price_cents ? 'margin-risk' : 'margin-ok'
"
>
{{ formatMoney(record.sell_price_cents - record.cost_price_cents) }}
</span>
</template>
@@ -117,7 +123,10 @@
checked-children="显示"
un-checked-children="隐藏"
:loading="visibilityLoadingId === record.id"
@change="(checked: boolean | string | number) => updateVisibility(record, Boolean(checked))"
@change="
(checked: boolean | string | number) =>
updateVisibility(record, Boolean(checked))
"
/>
</template>
<template v-else-if="column.key === 'remark'">
@@ -131,7 +140,11 @@
<template v-else-if="column.key === 'actions'">
<div class="table-actions-row">
<a-tooltip title="调整售价">
<a-button type="text" class="action-btn action-edit" @click="openPriceModal(record)">
<a-button
type="text"
class="action-btn action-edit"
@click="openPriceModal(record)"
>
<EditOutlined />
</a-button>
</a-tooltip>
@@ -175,7 +188,9 @@
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'user'">
<div class="resource-main">
<strong>{{ record.user_name || record.user_phone || `用户 ${record.user_id}` }}</strong>
<strong>
{{ record.user_name || record.user_phone || `用户 ${record.user_id}` }}
</strong>
<div class="resource-sub">
<code>U{{ record.user_id }}</code>
<span>{{ record.user_phone || '未绑定手机' }}</span>
@@ -191,7 +206,9 @@
</template>
<template v-else-if="column.key === 'ledger'">
<div>{{ record.ledger_entries }} </div>
<div class="muted">{{ record.last_ledger_at ? formatDate(record.last_ledger_at) : '暂无账单' }}</div>
<div class="muted">
{{ record.last_ledger_at ? formatDate(record.last_ledger_at) : '暂无账单' }}
</div>
</template>
<template v-else-if="column.key === 'updated_at'">
{{ formatDate(record.updated_at) }}
@@ -215,8 +232,12 @@
<div v-if="priceTarget" class="edit-stack">
<a-descriptions :column="1" size="small" bordered>
<a-descriptions-item label="资源">{{ priceTarget.name }}</a-descriptions-item>
<a-descriptions-item label="成本">{{ formatMoney(priceTarget.cost_price_cents) }}</a-descriptions-item>
<a-descriptions-item label="当前售价">{{ formatSellPrice(priceTarget.sell_price_cents) }}</a-descriptions-item>
<a-descriptions-item label="成本">
{{ formatMoney(priceTarget.cost_price_cents) }}
</a-descriptions-item>
<a-descriptions-item label="当前售价">
{{ formatSellPrice(priceTarget.sell_price_cents) }}
</a-descriptions-item>
</a-descriptions>
<a-form layout="vertical">
<a-form-item label="平台售价">
@@ -235,16 +256,26 @@
>
<div v-if="walletTarget" class="edit-stack">
<a-descriptions :column="1" size="small" bordered>
<a-descriptions-item label="客户">{{ walletTarget.user_name || walletTarget.user_phone || walletTarget.user_id }}</a-descriptions-item>
<a-descriptions-item label="租户">{{ walletTarget.tenant_name || walletTarget.tenant_id }}</a-descriptions-item>
<a-descriptions-item label="当前余额">{{ formatMoney(walletTarget.balance_cents) }}</a-descriptions-item>
<a-descriptions-item label="客户">
{{ walletTarget.user_name || walletTarget.user_phone || walletTarget.user_id }}
</a-descriptions-item>
<a-descriptions-item label="租户">
{{ walletTarget.tenant_name || walletTarget.tenant_id }}
</a-descriptions-item>
<a-descriptions-item label="当前余额">
{{ formatMoney(walletTarget.balance_cents) }}
</a-descriptions-item>
</a-descriptions>
<a-form layout="vertical">
<a-form-item label="调整金额">
<a-input v-model:value="walletForm.yuan" prefix="¥" placeholder="正数充值,负数扣减" />
</a-form-item>
<a-form-item label="备注">
<a-textarea v-model:value="walletForm.note" :rows="3" placeholder="账单备注,客户可见" />
<a-textarea
v-model:value="walletForm.note"
:rows="3"
placeholder="账单备注,客户可见"
/>
</a-form-item>
</a-form>
</div>
+152 -59
View File
@@ -29,7 +29,8 @@
<div class="metric-content">
<div class="metric-label">子文件夹</div>
<div class="metric-value">
{{ folderRows.length }} <span class="metric-unit"></span>
{{ folderRows.length }}
<span class="metric-unit"></span>
</div>
</div>
</div>
@@ -38,7 +39,8 @@
<div class="metric-content">
<div class="metric-label">对象文件数</div>
<div class="metric-value">
{{ rows.length }} <span class="metric-unit"></span>
{{ rows.length }}
<span class="metric-unit"></span>
</div>
</div>
</div>
@@ -68,7 +70,7 @@
{{ part.name }}
</a-button>
</template>
<div class="path-actions">
<a-tooltip title="复制当前目录路径">
<a-button type="text" size="small" class="path-action-btn" @click="copyCurrentPath">
@@ -82,7 +84,7 @@
</a-tooltip>
</div>
</div>
<div v-else class="breadcrumb-edit-container">
<span class="edit-prefix-label">路径编辑:</span>
<a-input
@@ -102,9 +104,7 @@
<a-button type="primary" size="small" class="path-save-btn" @click="saveEditedPath">
跳转
</a-button>
<a-button size="small" class="path-cancel-btn" @click="cancelEditedPath">
取消
</a-button>
<a-button size="small" class="path-cancel-btn" @click="cancelEditedPath">取消</a-button>
</a-space>
</div>
</div>
@@ -123,14 +123,18 @@
>
<template #prefix><SearchOutlined class="search-icon" /></template>
</a-input>
<a-tooltip title="勾选后将检索当前目录下所有层级的子文件;未勾选则仅展示当前一层。">
<a-checkbox v-model:checked="filter.recursive" @change="resetAndReload" class="oss-recursive-checkbox">
<a-checkbox
v-model:checked="filter.recursive"
@change="resetAndReload"
class="oss-recursive-checkbox"
>
递归搜索所有子目录
</a-checkbox>
</a-tooltip>
</div>
<div class="toolbar-right">
<a-space wrap>
<a-button @click="openCreateFolder" class="toolbar-secondary-btn">
@@ -150,7 +154,7 @@
</div>
<!-- Sliding Floating Batch Actions Panel -->
<div :class="['batch-ops-panel', { 'show': selectedRowKeys.length > 0 }]">
<div :class="['batch-ops-panel', { show: selectedRowKeys.length > 0 }]">
<div class="batch-info">
<span class="selected-badge">{{ selectedRowKeys.length }}</span>
<span class="selected-text">项已选择</span>
@@ -177,11 +181,19 @@
<template #icon><DownloadOutlined /></template>
批量下载
</a-button>
<a-button type="text" danger size="small" class="batch-action-btn-styled batch-delete-btn" @click="confirmBulkDelete">
<a-button
type="text"
danger
size="small"
class="batch-action-btn-styled batch-delete-btn"
@click="confirmBulkDelete"
>
<template #icon><DeleteOutlined /></template>
批量删除
</a-button>
<a-button size="small" type="link" class="batch-cancel-link" @click="clearSelection">取消选择</a-button>
<a-button size="small" type="link" class="batch-cancel-link" @click="clearSelection">
取消选择
</a-button>
</a-space>
</div>
@@ -200,13 +212,17 @@
<!-- Column 1: Object Key / Folder Name -->
<template v-if="column.key === 'key'">
<!-- Folder representation -->
<button v-if="record.kind === 'folder'" class="key-button folder-entry-btn" @click="enterFolder(record.key)">
<button
v-if="record.kind === 'folder'"
class="key-button folder-entry-btn"
@click="enterFolder(record.key)"
>
<div class="icon-wrapper folder-icon">
<FolderOutlined />
</div>
<span class="folder-name-text">{{ folderName(record.key) }}</span>
</button>
<!-- File representation -->
<div v-else class="object-key-cell">
<div :class="['icon-wrapper', `file-icon-${getFileIconType(record.key)}`]">
@@ -219,7 +235,9 @@
<FileOutlined v-else />
</div>
<div class="file-name-info">
<strong class="file-name-title" :title="fileName(record.key)">{{ fileName(record.key) }}</strong>
<strong class="file-name-title" :title="fileName(record.key)">
{{ fileName(record.key) }}
</strong>
<span class="file-full-key" :title="record.key">{{ record.key }}</span>
</div>
</div>
@@ -234,7 +252,11 @@
<!-- Column 3: Storage Type / Class -->
<template v-else-if="column.key === 'type'">
<span v-if="record.kind === 'folder'" class="folder-type-tag">目录</span>
<a-tag v-else :color="getTagColor(record.content_type || record.storage_class)" class="oss-type-tag">
<a-tag
v-else
:color="getTagColor(record.content_type || record.storage_class)"
class="oss-type-tag"
>
{{ record.content_type || record.storage_class || 'object' }}
</a-tag>
</template>
@@ -249,7 +271,13 @@
<template v-else-if="column.key === 'actions'">
<div v-if="record.kind === 'object'" class="table-actions-row">
<a-tooltip title="复制访问地址">
<a-button type="text" shape="circle" size="small" class="action-btn action-btn-primary" @click="copyURL(record.key)">
<a-button
type="text"
shape="circle"
size="small"
class="action-btn action-btn-primary"
@click="copyURL(record.key)"
>
<template #icon><CopyOutlined /></template>
</a-button>
</a-tooltip>
@@ -265,9 +293,14 @@
<template #icon><DownloadOutlined /></template>
</a-button>
</a-tooltip>
<a-dropdown :trigger="['click']" placement="bottomRight">
<a-button type="text" shape="circle" size="small" class="action-btn action-btn-more">
<a-button
type="text"
shape="circle"
size="small"
class="action-btn action-btn-more"
>
<template #icon><EllipsisOutlined /></template>
</a-button>
<template #overlay>
@@ -291,7 +324,7 @@
</template>
</a-dropdown>
</div>
<!-- Folder actions -->
<div v-else class="table-actions-row">
<a-tooltip title="删除目录">
@@ -357,7 +390,12 @@
<a-descriptions bordered :column="1" size="small" class="detail-desc-list">
<a-descriptions-item label="Object Key">
<span class="detail-key-text">{{ detail.key }}</span>
<a-button type="link" size="small" class="copy-inline-btn" @click="copyText(detail.key)">
<a-button
type="link"
size="small"
class="copy-inline-btn"
@click="copyText(detail.key)"
>
复制
</a-button>
</a-descriptions-item>
@@ -376,8 +414,15 @@
</a-descriptions-item>
<a-descriptions-item label="公网访问地址">
<div v-if="detail.public_url" class="url-detail-block">
<a :href="detail.public_url" target="_blank" class="detail-url-link">{{ detail.public_url }}</a>
<a-button type="primary" size="small" class="copy-url-btn" @click="copyText(detail.public_url)">
<a :href="detail.public_url" target="_blank" class="detail-url-link">
{{ detail.public_url }}
</a>
<a-button
type="primary"
size="small"
class="copy-url-btn"
@click="copyText(detail.public_url)"
>
复制公网URL
</a-button>
</div>
@@ -397,10 +442,17 @@
class="oss-upload-drawer"
>
<a-form layout="vertical" :model="uploadForm" class="upload-drawer-form">
<a-form-item label="目标 Object Key / 目录路径" required extra="默认采用当前面包屑所在目录。如果包含新子目录斜杠(如 images/test.png),将自动创建多级虚拟文件夹。">
<a-input v-model:value="uploadForm.key" placeholder="例如: images/example.png 或 example.png" />
<a-form-item
label="目标 Object Key / 目录路径"
required
extra="默认采用当前面包屑所在目录。如果包含新子目录斜杠(如 images/test.png),将自动创建多级虚拟文件夹。"
>
<a-input
v-model:value="uploadForm.key"
placeholder="例如: images/example.png 或 example.png"
/>
</a-form-item>
<a-form-item label="拖拽或选择文件" required>
<a-upload-dragger
:before-upload="beforeUpload"
@@ -417,17 +469,30 @@
<!-- File Chosen Card -->
<transition name="fade">
<div v-if="uploadForm.file" class="file-picked-card">
<div :class="['file-picked-icon-wrapper', `file-icon-${getFileIconType(uploadForm.file.name)}`]">
<div
:class="[
'file-picked-icon-wrapper',
`file-icon-${getFileIconType(uploadForm.file.name)}`,
]"
>
<FileZipOutlined v-if="getFileIconType(uploadForm.file.name) === 'zip'" />
<FileImageOutlined v-else-if="getFileIconType(uploadForm.file.name) === 'image'" />
<FilePdfOutlined v-else-if="getFileIconType(uploadForm.file.name) === 'pdf'" />
<FileOutlined v-else />
</div>
<div class="file-picked-info">
<strong class="file-picked-name" :title="uploadForm.file.name">{{ uploadForm.file.name }}</strong>
<strong class="file-picked-name" :title="uploadForm.file.name">
{{ uploadForm.file.name }}
</strong>
<span class="file-picked-size">{{ formatBytes(uploadForm.file.size) }}</span>
</div>
<a-button type="text" danger size="small" class="file-picked-remove" @click="uploadForm.file = null">
<a-button
type="text"
danger
size="small"
class="file-picked-remove"
@click="uploadForm.file = null"
>
清除
</a-button>
</div>
@@ -437,7 +502,10 @@
<a-collapse ghost class="upload-advanced-collapse">
<a-collapse-panel key="advanced" header="高级选项设置">
<a-form-item label="手动指定 Content-Type (可选)">
<a-input v-model:value="uploadForm.content_type" placeholder="例如: image/png,留空将由系统自动识别" />
<a-input
v-model:value="uploadForm.content_type"
placeholder="例如: image/png,留空将由系统自动识别"
/>
</a-form-item>
<a-form-item class="checkbox-form-item">
<a-checkbox v-model:checked="uploadForm.overwrite">允许直接覆盖同名对象</a-checkbox>
@@ -449,7 +517,12 @@
<template #footer>
<div class="drawer-footer">
<a-button @click="closeUpload" class="footer-btn-secondary">取消</a-button>
<a-button type="primary" :loading="uploading" @click="submitUpload" class="footer-btn-primary">
<a-button
type="primary"
:loading="uploading"
@click="submitUpload"
class="footer-btn-primary"
>
开始上传
</a-button>
</div>
@@ -467,8 +540,16 @@
class="oss-create-folder-modal"
>
<a-form layout="vertical">
<a-form-item label="文件夹名称" required extra="会在 OSS 中真实创建一个以 / 结尾的空目录对象,创建后可立即在当前目录看到。">
<a-input v-model:value="newFolderName" placeholder="例如: releases 或 images" allow-clear />
<a-form-item
label="文件夹名称"
required
extra="会在 OSS 中真实创建一个以 / 结尾的空目录对象,创建后可立即在当前目录看到。"
>
<a-input
v-model:value="newFolderName"
placeholder="例如: releases 或 images"
allow-clear
/>
</a-form-item>
</a-form>
</a-modal>
@@ -669,8 +750,12 @@ function jumpToPath(path: string) {
// Row selection & Batch operations
const selectedRowKeys = ref<string[]>([])
const selectedRows = ref<TableRow[]>([])
const selectedObjectCount = computed(() => selectedRows.value.filter((row) => row.kind === 'object').length)
const selectedFolderCount = computed(() => selectedRows.value.filter((row) => row.kind === 'folder').length)
const selectedObjectCount = computed(
() => selectedRows.value.filter((row) => row.kind === 'object').length,
)
const selectedFolderCount = computed(
() => selectedRows.value.filter((row) => row.kind === 'folder').length,
)
const batchDeleteDescription = computed(() => {
if (selectedFolderCount.value > 0) {
return '选中的目录会按前缀递归删除其下所有文件和空目录占位对象,删除后无法恢复。'
@@ -716,7 +801,10 @@ async function mapWithConcurrency<T, R>(
}
function formatKeyList(keys: string[]): string {
const visible = keys.slice(0, 3).map((key) => fileName(key)).join('、')
const visible = keys
.slice(0, 3)
.map((key) => fileName(key))
.join('、')
return keys.length > 3 ? `${visible}${keys.length}` : visible
}
@@ -758,12 +846,10 @@ async function copyText(text: string) {
// Bulk Actions Implementation
async function bulkCopyURLs() {
const keys = selectedRows.value
.filter((row) => row.kind === 'object')
.map((row) => row.key)
const keys = selectedRows.value.filter((row) => row.kind === 'object').map((row) => row.key)
if (keys.length === 0) return
try {
const results = await mapWithConcurrency(keys, bulkConcurrency, async (key) => {
const result = await objectStorageApi.resolveDownloadURL(key)
@@ -776,7 +862,9 @@ async function bulkCopyURLs() {
}
await navigator.clipboard.writeText(urls.join('\n'))
if (failures.length > 0) {
message.warning(`已复制 ${urls.length} 个访问地址,失败 ${failures.length} 个:${formatKeyList(failures)}`)
message.warning(
`已复制 ${urls.length} 个访问地址,失败 ${failures.length} 个:${formatKeyList(failures)}`,
)
} else {
message.success(`已复制全部 ${urls.length} 个对象的访问地址到剪贴板`)
}
@@ -787,13 +875,11 @@ async function bulkCopyURLs() {
}
async function bulkDownload() {
const keys = selectedRows.value
.filter((row) => row.kind === 'object')
.map((row) => row.key)
const keys = selectedRows.value.filter((row) => row.kind === 'object').map((row) => row.key)
if (keys.length === 0) return
message.info(`正在交给浏览器下载 ${keys.length} 个文件...`)
let successCount = 0
const failures: string[] = []
try {
@@ -814,9 +900,11 @@ async function bulkDownload() {
failures.push(item.key)
}
}
if (failures.length > 0) {
message.warning(`批量下载已提交 ${successCount} 个,失败 ${failures.length} 个:${formatKeyList(failures)}`)
message.warning(
`批量下载已提交 ${successCount} 个,失败 ${failures.length} 个:${formatKeyList(failures)}`,
)
} else {
message.success('批量下载任务已就绪')
}
@@ -835,9 +923,9 @@ async function bulkDelete() {
.filter((row) => row.kind === 'object')
.map((row) => row.key)
.filter((key) => !folderPrefixes.some((prefix) => key.startsWith(prefix)))
if (folderPrefixes.length === 0 && objectKeys.length === 0) return
let shouldReload = false
try {
loading.value = true
@@ -860,11 +948,13 @@ async function bulkDelete() {
const { values, failures: failedTasks } = splitBulkResults(results)
const successCount = values.length
const failures = failedTasks.map((item) => item.key)
if (failures.length === 0) {
message.success(`成功删除选中的全部 ${successCount}`)
} else {
message.warning(`批量删除完毕:成功 ${successCount} 个,失败 ${failures.length} 个:${formatKeyList(failures)}`)
message.warning(
`批量删除完毕:成功 ${successCount} 个,失败 ${failures.length} 个:${formatKeyList(failures)}`,
)
}
shouldReload = successCount > 0
closeBatchDelete()
@@ -928,7 +1018,7 @@ async function submitCreateFolder() {
message.warning('文件夹名称不能包含层级斜杠 /')
return
}
const newPath = currentPrefix.value ? `${currentPrefix.value}${name}/` : `${name}/`
creatingFolder.value = true
try {
@@ -979,7 +1069,8 @@ function getFileIconType(key: string): 'zip' | 'image' | 'pdf' | 'doc' | 'excel'
if (['zip', 'rar', 'tar', 'gz', '7z'].includes(ext)) return 'zip'
if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico'].includes(ext)) return 'image'
if (['pdf'].includes(ext)) return 'pdf'
if (['txt', 'md', 'html', 'css', 'js', 'ts', 'vue', 'json', 'yaml', 'xml'].includes(ext)) return 'doc'
if (['txt', 'md', 'html', 'css', 'js', 'ts', 'vue', 'json', 'yaml', 'xml'].includes(ext))
return 'doc'
if (['xls', 'xlsx', 'csv'].includes(ext)) return 'excel'
if (['doc', 'docx'].includes(ext)) return 'word'
return 'file'
@@ -995,7 +1086,8 @@ function getTagColor(typeStr: string | null | undefined): string {
if (lower.includes('image')) return 'green'
if (lower.includes('zip') || lower.includes('compressed')) return 'purple'
if (lower.includes('pdf')) return 'red'
if (lower.includes('json') || lower.includes('javascript') || lower.includes('text')) return 'blue'
if (lower.includes('json') || lower.includes('javascript') || lower.includes('text'))
return 'blue'
if (lower.includes('standard')) return 'geekblue'
return 'default'
}
@@ -1636,7 +1728,7 @@ onMounted(() => {
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 100px;
padding: 10px 24px;
box-shadow:
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.3),
0 8px 10px -6px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
@@ -1924,7 +2016,8 @@ onMounted(() => {
}
@keyframes float-anim {
0%, 100% {
0%,
100% {
transform: translateY(0);
}
50% {
+97 -22
View File
@@ -78,7 +78,9 @@
<div class="job-cell">
<div class="job-title">
<span>{{ record.display_name }}</span>
<a-tag :color="categoryColor(record.category)">{{ categoryLabel(record.category) }}</a-tag>
<a-tag :color="categoryColor(record.category)">
{{ categoryLabel(record.category) }}
</a-tag>
</div>
<code>{{ record.job_key }}</code>
<div class="muted">{{ record.description || '—' }}</div>
@@ -108,7 +110,9 @@
</template>
<template v-else-if="column.key === 'stats'">
<div class="mono-line">{{ statsSummary(record.last_run?.stats) }}</div>
<div v-if="record.pending_triggers" class="muted">待触发 {{ record.pending_triggers }}</div>
<div v-if="record.pending_triggers" class="muted">
待触发 {{ record.pending_triggers }}
</div>
</template>
<template v-else-if="column.key === 'actions'">
<div class="table-actions-row">
@@ -118,14 +122,28 @@
</a-button>
</a-tooltip>
<a-tooltip title="立即运行">
<a-popconfirm title="确认立即运行该调度任务?" ok-text="运行" cancel-text="取消" @confirm="triggerJob(record, 'run')">
<a-button type="text" class="action-btn action-play" :loading="triggeringKey === `${record.job_key}:run`">
<a-popconfirm
title="确认立即运行该调度任务?"
ok-text="运行"
cancel-text="取消"
@confirm="triggerJob(record, 'run')"
>
<a-button
type="text"
class="action-btn action-play"
:loading="triggeringKey === `${record.job_key}:run`"
>
<PlayCircleOutlined />
</a-button>
</a-popconfirm>
</a-tooltip>
<a-tooltip title="Dry-run">
<a-button type="text" class="action-btn action-warn" :loading="triggeringKey === `${record.job_key}:dry-run`" @click="triggerJob(record, 'dry-run')">
<a-button
type="text"
class="action-btn action-warn"
:loading="triggeringKey === `${record.job_key}:dry-run`"
@click="triggerJob(record, 'dry-run')"
>
<ExperimentOutlined />
</a-button>
</a-tooltip>
@@ -143,7 +161,11 @@
<a-drawer v-model:open="editOpen" width="620" :title="editTitle" :destroy-on-close="true">
<a-form v-if="editing" layout="vertical">
<a-form-item label="启用状态">
<a-switch v-model:checked="editForm.enabled" checked-children="启用" un-checked-children="暂停" />
<a-switch
v-model:checked="editForm.enabled"
checked-children="启用"
un-checked-children="暂停"
/>
</a-form-item>
<a-form-item label="调度类型">
<a-segmented v-model:value="editForm.schedule_type" :options="scheduleTypeOptions" />
@@ -152,23 +174,45 @@
<a-input v-model:value="editForm.cron_expr" placeholder="例如 0 30 2 * * *" />
</a-form-item>
<a-form-item label="间隔秒数">
<a-input-number v-model:value="editForm.interval_seconds" :min="1" :max="86400" style="width: 100%" />
<a-input-number
v-model:value="editForm.interval_seconds"
:min="1"
:max="86400"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="超时秒数">
<a-input-number v-model:value="editForm.timeout_seconds" :min="1" :max="3600" style="width: 100%" />
<a-input-number
v-model:value="editForm.timeout_seconds"
:min="1"
:max="3600"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="批大小">
<a-input-number v-model:value="editForm.batch_size" :min="1" :max="100000" style="width: 100%" />
<a-input-number
v-model:value="editForm.batch_size"
:min="1"
:max="100000"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="时区">
<a-input v-model:value="editForm.timezone" />
</a-form-item>
<a-form-item label="高级配置 JSON">
<a-textarea v-model:value="editConfigText" :rows="12" spellcheck="false" class="config-editor" />
<a-textarea
v-model:value="editConfigText"
:rows="12"
spellcheck="false"
class="config-editor"
/>
</a-form-item>
<div class="drawer-actions">
<a-button @click="editOpen = false">取消</a-button>
<a-button type="primary" :loading="savingKey === editing.job_key" @click="saveEdit">保存</a-button>
<a-button type="primary" :loading="savingKey === editing.job_key" @click="saveEdit">
保存
</a-button>
</div>
</a-form>
</a-drawer>
@@ -185,7 +229,9 @@
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="runStatusColor(record.status)">{{ runStatusLabel(record.status) }}</a-tag>
<a-tag :color="runStatusColor(record.status)">
{{ runStatusLabel(record.status) }}
</a-tag>
<div class="muted">{{ record.trigger_type }}</div>
</template>
<template v-else-if="column.key === 'time'">
@@ -202,8 +248,19 @@
</a-table>
</a-drawer>
<a-drawer v-model:open="instancesOpen" width="720" title="Scheduler 实例" :destroy-on-close="true">
<a-table :columns="instanceColumns" :data-source="instances" :loading="instancesLoading" row-key="instance_id" :pagination="false">
<a-drawer
v-model:open="instancesOpen"
width="720"
title="Scheduler 实例"
:destroy-on-close="true"
>
<a-table
:columns="instanceColumns"
:data-source="instances"
:loading="instancesLoading"
row-key="instance_id"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'instance'">
<div class="mono-line">{{ record.instance_id }}</div>
@@ -378,9 +435,15 @@ const runPagination = computed<TablePaginationConfig>(() => ({
const enabledCount = computed(() => rows.value.filter((row) => row.enabled).length)
const runningCount = computed(() => rows.value.filter((row) => row.running_run).length)
const failedCount = computed(() => rows.value.filter((row) => row.last_run?.status === 'failed').length)
const editTitle = computed(() => (editing.value ? `编辑 · ${editing.value.display_name}` : '编辑调度任务'))
const runsTitle = computed(() => (runsJob.value ? `运行记录 · ${runsJob.value.display_name}` : '运行记录'))
const failedCount = computed(
() => rows.value.filter((row) => row.last_run?.status === 'failed').length,
)
const editTitle = computed(() =>
editing.value ? `编辑 · ${editing.value.display_name}` : '编辑调度任务',
)
const runsTitle = computed(() =>
runsJob.value ? `运行记录 · ${runsJob.value.display_name}` : '运行记录',
)
async function reload() {
loading.value = true
@@ -494,10 +557,13 @@ async function loadRuns() {
if (!runsJob.value) return
runsLoading.value = true
try {
const result = await http.get<ListResult<SchedulerRun>>(`/scheduler/jobs/${runsJob.value.job_key}/runs`, {
page: runPage.value,
size: runSize.value,
})
const result = await http.get<ListResult<SchedulerRun>>(
`/scheduler/jobs/${runsJob.value.job_key}/runs`,
{
page: runPage.value,
size: runSize.value,
},
)
runRows.value = result.items
runTotal.value = result.total
} catch (error) {
@@ -573,7 +639,16 @@ function runStatusColor(value?: string | null): string {
function statsSummary(stats?: Record<string, unknown> | null): string {
if (!stats) return '—'
const pairs = Object.entries(stats)
.filter(([key]) => ['deleted_count', 'candidate_count', 'created_task_count', 'claimed_count', 'expired_task_count', 'anomaly_count'].includes(key))
.filter(([key]) =>
[
'deleted_count',
'candidate_count',
'created_task_count',
'claimed_count',
'expired_task_count',
'anomaly_count',
].includes(key),
)
.map(([key, value]) => `${key}=${String(value)}`)
return pairs.length ? pairs.join(' · ') : '—'
}
@@ -123,19 +123,21 @@
</template>
<script setup lang="ts">
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, RocketOutlined } from '@ant-design/icons-vue'
import {
DeleteOutlined,
EditOutlined,
PlusOutlined,
ReloadOutlined,
RocketOutlined,
} from '@ant-design/icons-vue'
import type { TableColumnsType } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import {
levelColor,
levelLabel,
platformLabel,
} from '@/lib/compliance-display'
import { complianceApi, type OpsComplianceDictionary } from '@/lib/compliance'
import { levelColor, levelLabel, platformLabel } from '@/lib/compliance-display'
import { showOpsError } from '@/lib/errors'
const router = useRouter()
@@ -196,7 +196,12 @@
</template>
</a-drawer>
<a-drawer v-model:open="batchDrawerOpen" title="批量导入词条" width="520" :destroy-on-close="true">
<a-drawer
v-model:open="batchDrawerOpen"
title="批量导入词条"
width="520"
:destroy-on-close="true"
>
<a-alert
class="create-terms-alert"
type="info"
@@ -222,17 +227,13 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import PlatformScopePicker from '@/components/compliance/PlatformScopePicker.vue'
import {
complianceLevelOptions,
levelColor,
levelLabel,
} from '@/lib/compliance-display'
import {
complianceApi,
type ComplianceLevel,
type OpsComplianceDictionary,
type OpsComplianceTerm,
} from '@/lib/compliance'
import { complianceLevelOptions, levelColor, levelLabel } from '@/lib/compliance-display'
import { showOpsError } from '@/lib/errors'
type MatchType = 'exact' | 'regex'
@@ -463,7 +464,10 @@ async function rollbackDictionary() {
if (!dictionaryId.value || !rollbackVersion.value) return
rollingBack.value = true
try {
dictionary.value = await complianceApi.rollbackDictionary(dictionaryId.value, rollbackVersion.value)
dictionary.value = await complianceApi.rollbackDictionary(
dictionaryId.value,
rollbackVersion.value,
)
message.success('词库已回滚')
await loadDictionary()
} catch (error) {
@@ -48,7 +48,11 @@
<template v-if="review.status === 'pending'">
<a-form class="decision-form" layout="vertical">
<a-form-item label="决策理由" required>
<a-textarea v-model:value="decisionReason" :rows="5" placeholder="填写通过或驳回原因" />
<a-textarea
v-model:value="decisionReason"
:rows="5"
placeholder="填写通过或驳回原因"
/>
</a-form-item>
<a-space class="decision-actions">
<a-popconfirm
@@ -116,14 +120,15 @@
</template>
<script setup lang="ts">
import type { ComplianceManualReview, ComplianceViolation } from '@geo/shared-types'
import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue'
import type { ComplianceManualReview, ComplianceViolation } from '@geo/shared-types'
import type { TableColumnsType } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { complianceApi } from '@/lib/compliance'
import {
levelColor,
levelLabel,
@@ -133,7 +138,6 @@ import {
reviewStatusLabel,
sourceLabel,
} from '@/lib/compliance-display'
import { complianceApi } from '@/lib/compliance'
import { showOpsError } from '@/lib/errors'
const route = useRoute()
@@ -45,7 +45,9 @@
<div class="primary-cell">
<strong>{{ record.article_title || `文章 #${record.article_id}` }}</strong>
<span>
租户 {{ record.tenant_name || `#${record.tenant_id}` }} · 版本 #{{ record.article_version_id }}
租户 {{ record.tenant_name || `#${record.tenant_id}` }} · 版本 #{{
record.article_version_id
}}
</span>
</div>
</template>
@@ -86,20 +88,20 @@
</template>
<script setup lang="ts">
import type { ComplianceManualReview } from '@geo/shared-types'
import { ReloadOutlined } from '@ant-design/icons-vue'
import type { ComplianceManualReview } from '@geo/shared-types'
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue'
import dayjs from 'dayjs'
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { complianceApi } from '@/lib/compliance'
import {
levelColor,
platformListLabel,
reviewStatusColor,
reviewStatusLabel,
} from '@/lib/compliance-display'
import { complianceApi } from '@/lib/compliance'
import { showOpsError } from '@/lib/errors'
const router = useRouter()
@@ -116,13 +116,13 @@ import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
import { computed, onMounted, reactive, ref } from 'vue'
import { modeColor, modeLabel } from '@/lib/compliance-display'
import {
complianceApi,
type ComplianceMode,
type CompliancePolicy,
type OpsComplianceDictionary,
} from '@/lib/compliance'
import { modeColor, modeLabel } from '@/lib/compliance-display'
import { showOpsError } from '@/lib/errors'
const policy = ref<CompliancePolicy | null>(null)
@@ -47,7 +47,11 @@
<template v-if="column.key === 'record'">
<div class="primary-cell">
<strong>记录 #{{ record.record_id }}</strong>
<span>文章 #{{ record.article_id || '--' }} · 版本 #{{ record.article_version_id || '--' }}</span>
<span>
文章 #{{ record.article_id || '--' }} · 版本 #{{
record.article_version_id || '--'
}}
</span>
</div>
</template>
<template v-else-if="column.key === 'decision'">
@@ -86,9 +90,7 @@
<template v-else-if="column.key === 'checked_at'">
{{ formatDate(record.checked_at) }}
</template>
<template v-else-if="column.key === 'duration'">
{{ record.duration_ms }}ms
</template>
<template v-else-if="column.key === 'duration'">{{ record.duration_ms }}ms</template>
</template>
</a-table>
</div>
@@ -96,12 +98,13 @@
</template>
<script setup lang="ts">
import type { ComplianceCheckResult } from '@geo/shared-types'
import { ReloadOutlined } from '@ant-design/icons-vue'
import type { ComplianceCheckResult } from '@geo/shared-types'
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue'
import dayjs from 'dayjs'
import { computed, onMounted, reactive, ref } from 'vue'
import { complianceApi } from '@/lib/compliance'
import {
decisionColor,
decisionLabel,
@@ -112,7 +115,6 @@ import {
reviewStatusColor,
reviewStatusLabel,
} from '@/lib/compliance-display'
import { complianceApi } from '@/lib/compliance'
import { showOpsError } from '@/lib/errors'
const rows = ref<ComplianceCheckResult[]>([])
@@ -76,8 +76,8 @@
import { ReloadOutlined } from '@ant-design/icons-vue'
import { computed, onMounted, ref } from 'vue'
import { levelColor, levelLabel, sourceLabel } from '@/lib/compliance-display'
import { complianceApi, type ComplianceStats } from '@/lib/compliance'
import { levelColor, levelLabel, sourceLabel } from '@/lib/compliance-display'
import { showOpsError } from '@/lib/errors'
const stats = ref<ComplianceStats | null>(null)