747 lines
23 KiB
Vue
747 lines
23 KiB
Vue
<script setup lang="ts">
|
||
/** 查询 ASIN · 像素复刻旧版 admin.html panel-query-asin(自绘:面板头操作组、筛选+导出 XLSX、原生 rowspan 表格+可复制 ASIN、旧式分页)。
|
||
* script 逻辑沿用现有 Vue 实现(新增级联/配置抽屉/导入轮询/导出)。 */
|
||
import { computed, onMounted, reactive, ref } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import CopyText from '@/components/CopyText.vue'
|
||
import { createQueryAsin, fetchQueryAsinList, fetchShopNamesByGroup } from './query-asin-api.ts'
|
||
import { QUERY_ASIN_COUNTRIES, queryAsinDisplayRows, type QueryAsinItem } from './query-asin-model.ts'
|
||
import { asinCountryLabel } from './asin-country.ts'
|
||
import { createQueryAsinFilterState, type QueryAsinFilterState } from './query-asin-filter.ts'
|
||
import { deleteQueryAsinCountryAsin, updateQueryAsinCountryAsin } from './query-asin-detail-api.ts'
|
||
import { fetchQueryAsinImportProgress, startQueryAsinImport } from './query-asin-import-api.ts'
|
||
import { fetchQueryAsinDeleteImportProgress, startQueryAsinDeleteImport } from './query-asin-delete-import-api.ts'
|
||
import { isAllowedExcelImportFile } from './import-progress-model.ts'
|
||
import { fetchShopManageGroups } from '../shop/shop-manage-api.ts'
|
||
import type { ShopGroupOption } from '../shop/shop-dto.ts'
|
||
|
||
const loading = ref(false)
|
||
const rows = ref<QueryAsinItem[]>([])
|
||
const total = ref(0)
|
||
const page = ref(1)
|
||
const pageSize = ref(15)
|
||
const groups = ref<ShopGroupOption[]>([])
|
||
const filter = reactive<QueryAsinFilterState>(createQueryAsinFilterState())
|
||
const jumpPage = ref('')
|
||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
|
||
|
||
/** 纵向展示行:分组/店铺/操作 按 rowspan 合并,逐国一行(对齐 admin.js renderQueryAsinRows)。 */
|
||
const displayRows = computed(() => queryAsinDisplayRows(rows.value, (page.value - 1) * pageSize.value + 1))
|
||
|
||
function asinOf(row: QueryAsinItem, country: string): string {
|
||
const map: Record<string, string> = { DE: row.asinDe, UK: row.asinUk, FR: row.asinFr, IT: row.asinIt, ES: row.asinEs }
|
||
return map[country] || ''
|
||
}
|
||
|
||
// ---- 行内配置抽屉 ----
|
||
const drawerVisible = ref(false)
|
||
const drawerItem = ref<QueryAsinItem | null>(null)
|
||
const drawerSaving = ref(false)
|
||
const draftAsin = reactive<Record<string, string>>({ DE: '', UK: '', FR: '', IT: '', ES: '' })
|
||
|
||
function openConfig(row: QueryAsinItem): void {
|
||
drawerItem.value = row
|
||
for (const country of QUERY_ASIN_COUNTRIES) {
|
||
draftAsin[country] = asinOf(row, country)
|
||
}
|
||
drawerVisible.value = true
|
||
}
|
||
|
||
function normalizeAsinInput(country: string): void {
|
||
draftAsin[country] = draftAsin[country].trim().toUpperCase()
|
||
}
|
||
|
||
async function saveConfig(): Promise<void> {
|
||
const item = drawerItem.value
|
||
if (!item) return
|
||
drawerSaving.value = true
|
||
const errors: string[] = []
|
||
try {
|
||
for (const country of QUERY_ASIN_COUNTRIES) {
|
||
const oldAsin = asinOf(item, country)
|
||
const next = (draftAsin[country] || '').trim().toUpperCase()
|
||
if (next === oldAsin) continue
|
||
try {
|
||
if (!next) {
|
||
if (oldAsin) await deleteQueryAsinCountryAsin(item.id, country)
|
||
} else {
|
||
await updateQueryAsinCountryAsin(item.id, country, next)
|
||
}
|
||
} catch (error) {
|
||
errors.push(`${asinCountryLabel(country)}:${error instanceof Error ? error.message : '保存失败'}`)
|
||
}
|
||
}
|
||
if (errors.length) {
|
||
ElMessage.error(errors.join(';'))
|
||
} else {
|
||
ElMessage.success('保存成功')
|
||
drawerVisible.value = false
|
||
drawerItem.value = null
|
||
await load()
|
||
}
|
||
} finally {
|
||
drawerSaving.value = false
|
||
}
|
||
}
|
||
|
||
// ---- 新增 ASIN 弹窗(对齐 admin.js btnOpenCreateQueryAsin/btnCreateQueryAsin) ----
|
||
const createVisible = ref(false)
|
||
const createGroupId = ref<number | null>(null)
|
||
const createShopName = ref('')
|
||
const createCountry = ref('')
|
||
const createAsin = ref('')
|
||
const createMsg = ref('')
|
||
const createMsgOk = ref(false)
|
||
const shopNames = ref<string[]>([])
|
||
const shopNamesLoading = ref(false)
|
||
const creating = ref(false)
|
||
const createAsinInputRef = ref<InstanceType<typeof import('element-plus').ElInput> | null>(null)
|
||
|
||
function openCreate(): void {
|
||
createGroupId.value = null
|
||
createShopName.value = ''
|
||
createCountry.value = ''
|
||
createAsin.value = ''
|
||
createMsg.value = ''
|
||
createMsgOk.value = false
|
||
shopNames.value = []
|
||
createVisible.value = true
|
||
}
|
||
|
||
async function onCreateGroupChange(): Promise<void> {
|
||
createShopName.value = ''
|
||
if (createGroupId.value == null) {
|
||
shopNames.value = []
|
||
return
|
||
}
|
||
shopNamesLoading.value = true
|
||
try {
|
||
shopNames.value = await fetchShopNamesByGroup(createGroupId.value)
|
||
} catch (error) {
|
||
shopNames.value = []
|
||
ElMessage.error(error instanceof Error ? error.message : '店铺列表加载失败')
|
||
} finally {
|
||
shopNamesLoading.value = false
|
||
}
|
||
}
|
||
|
||
function onCreateAsinInput(): void {
|
||
createAsin.value = createAsin.value.toUpperCase()
|
||
}
|
||
|
||
async function submitCreate(): Promise<void> {
|
||
createMsg.value = ''
|
||
createMsgOk.value = false
|
||
const groupId = createGroupId.value
|
||
const shopName = createShopName.value.trim()
|
||
const country = (createCountry.value || '').trim()
|
||
const asin = createAsin.value.trim().toUpperCase()
|
||
if (!groupId || !shopName || !country || !asin) {
|
||
createMsg.value = '请完整填写分组、店铺名、国家和 ASIN'
|
||
return
|
||
}
|
||
creating.value = true
|
||
try {
|
||
const message = await createQueryAsin({
|
||
groupId,
|
||
shopName,
|
||
countries: [country],
|
||
asin,
|
||
asinMappings: { [country]: asin },
|
||
})
|
||
// 对齐 admin:保留分组/店铺/国家,清空 ASIN 方便连续录入,成功后刷新列表。
|
||
createMsg.value = message
|
||
createMsgOk.value = true
|
||
createAsin.value = ''
|
||
createAsinInputRef.value?.focus()
|
||
await load()
|
||
} catch (error) {
|
||
createMsg.value = error instanceof Error ? error.message : '保存失败'
|
||
} finally {
|
||
creating.value = false
|
||
}
|
||
}
|
||
|
||
// ---- 导入添加 / 删除导入 ----
|
||
type ImportMode = 'add' | 'delete'
|
||
const importVisible = ref(false)
|
||
const importMode = ref<ImportMode>('add')
|
||
const importGroupId = ref<number | null>(null)
|
||
const importFile = ref<File | null>(null)
|
||
const importRunning = ref(false)
|
||
const importProgress = ref('')
|
||
|
||
function resetImport(): void {
|
||
importGroupId.value = null
|
||
importFile.value = null
|
||
importProgress.value = ''
|
||
importRunning.value = false
|
||
}
|
||
|
||
function openImportAdd(): void {
|
||
importMode.value = 'add'
|
||
resetImport()
|
||
importVisible.value = true
|
||
}
|
||
|
||
function openImportDelete(): void {
|
||
importMode.value = 'delete'
|
||
resetImport()
|
||
importVisible.value = true
|
||
}
|
||
|
||
function onPickImportFile(event: Event): void {
|
||
const input = event.target as HTMLInputElement
|
||
importFile.value = input.files?.[0] ?? null
|
||
}
|
||
|
||
function sleep(ms: number): Promise<void> {
|
||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||
}
|
||
|
||
async function submitImport(): Promise<void> {
|
||
const file = importFile.value
|
||
if (!file) {
|
||
ElMessage.warning('请选择 Excel 文件')
|
||
return
|
||
}
|
||
if (!isAllowedExcelImportFile(file.name)) {
|
||
ElMessage.warning('仅支持 .xlsx/.xls 文件')
|
||
return
|
||
}
|
||
if (importMode.value === 'delete') {
|
||
if (!window.confirm('确定按 Excel 中的分组、店铺名和国家 ASIN 批量删除吗?')) return
|
||
}
|
||
const mode = importMode.value
|
||
const groupId = importGroupId.value ?? undefined
|
||
importRunning.value = true
|
||
importProgress.value = '正在上传并解析文件…'
|
||
try {
|
||
const started = mode === 'add' ? await startQueryAsinImport(file, groupId) : await startQueryAsinDeleteImport(file, groupId)
|
||
const poll = mode === 'add' ? () => fetchQueryAsinImportProgress(started) : () => fetchQueryAsinDeleteImportProgress(started)
|
||
for (let i = 0; i < 300; i += 1) {
|
||
const progress = await poll()
|
||
if (progress.status === 'success') {
|
||
importVisible.value = false
|
||
resetImport()
|
||
ElMessage.success(mode === 'add' ? '导入完成' : '删除导入完成')
|
||
await load()
|
||
return
|
||
}
|
||
if (progress.status === 'failed') {
|
||
importVisible.value = false
|
||
resetImport()
|
||
ElMessage.error((progress as { errorMessage?: string }).errorMessage || (mode === 'add' ? '导入失败' : '删除导入失败'))
|
||
return
|
||
}
|
||
importProgress.value = progress.status === 'pending' ? '等待导入任务开始…' : '导入处理中…'
|
||
await sleep(1200)
|
||
}
|
||
throw new Error('查询导入进度超时,请稍后刷新列表确认结果')
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '导入失败')
|
||
} finally {
|
||
importRunning.value = false
|
||
}
|
||
}
|
||
|
||
// ---- 导出 ----
|
||
function doExport(): void {
|
||
const anchor = document.createElement('a')
|
||
anchor.href = '/api/admin/query-asins/export'
|
||
document.body.appendChild(anchor)
|
||
anchor.click()
|
||
anchor.remove()
|
||
ElMessage.success('导出文件已开始下载')
|
||
}
|
||
|
||
async function loadGroups() {
|
||
try {
|
||
groups.value = await fetchShopManageGroups()
|
||
} catch {
|
||
groups.value = []
|
||
}
|
||
}
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const result = await fetchQueryAsinList({
|
||
page: page.value,
|
||
pageSize: pageSize.value,
|
||
groupId: filter.groupId,
|
||
shopName: filter.shopName.trim() || undefined,
|
||
asin: filter.asin.trim() || undefined,
|
||
country: filter.country || undefined,
|
||
})
|
||
rows.value = result.items
|
||
total.value = result.total
|
||
page.value = result.page
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '数据加载失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function apply() {
|
||
page.value = 1
|
||
void load()
|
||
}
|
||
|
||
function changePage(next: number) {
|
||
if (next < 1 || next > totalPages.value) return
|
||
page.value = next
|
||
void load()
|
||
}
|
||
|
||
|
||
function changeSize(size: number) {
|
||
pageSize.value = size
|
||
page.value = 1
|
||
load()
|
||
}
|
||
|
||
function goJump() {
|
||
const n = Number.parseInt(jumpPage.value, 10)
|
||
if (Number.isNaN(n)) {
|
||
ElMessage.warning('请输入页码')
|
||
return
|
||
}
|
||
changePage(Math.min(Math.max(n, 1), totalPages.value))
|
||
}
|
||
|
||
onMounted(() => {
|
||
void loadGroups()
|
||
void load()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="query-view">
|
||
<section class="panel-box">
|
||
<div class="query-head">
|
||
<h3>店铺列表</h3>
|
||
<div class="query-head-actions">
|
||
<button class="btn" type="button" @click="openCreate">新增 ASIN</button>
|
||
<button class="btn btn-secondary" type="button" @click="openImportAdd">导入添加</button>
|
||
<button class="btn" type="button" @click="openImportDelete">导入删除</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row query-filter-row">
|
||
<div class="form-group">
|
||
<label>分组</label>
|
||
<el-select v-model="filter.groupId" class="admin-filter" filterable clearable placeholder="全部分组">
|
||
<el-option v-for="group in groups" :key="group.id" :label="group.groupName" :value="group.id" />
|
||
</el-select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>店铺名</label>
|
||
<input v-model="filter.shopName" type="text" placeholder="请输入店铺名" @keyup.enter="apply" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>国家</label>
|
||
<select v-model="filter.country">
|
||
<option value="">全部国家</option>
|
||
<option v-for="code in QUERY_ASIN_COUNTRIES" :key="code" :value="code">{{ asinCountryLabel(code) }}</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>ASIN</label>
|
||
<input v-model="filter.asin" type="text" placeholder="请输入 ASIN" @keyup.enter="apply" />
|
||
</div>
|
||
<button class="btn" type="button" @click="apply">查询</button>
|
||
<button class="btn btn-secondary" type="button" @click="doExport">导出 XLSX</button>
|
||
</div>
|
||
|
||
<div class="table-scroll query-asin-table-scroll">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 6%">序号</th>
|
||
<th style="width: 13%">分组</th>
|
||
<th style="width: 15%">店铺名</th>
|
||
<th style="width: 24%">ASIN</th>
|
||
<th style="width: 12%">国家</th>
|
||
<th style="width: 9%">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template v-if="displayRows.length">
|
||
<tr v-for="row in displayRows" :key="`${row.item.id}-${row.country}`">
|
||
<template v-if="row.isFirst">
|
||
<td :rowspan="row.rowspan">{{ row.rowNo }}</td>
|
||
<td :rowspan="row.rowspan">{{ row.item.groupName || '—' }}</td>
|
||
<td :rowspan="row.rowspan">{{ row.item.shopName }}</td>
|
||
</template>
|
||
<td>
|
||
<template v-if="row.asin">
|
||
<CopyText :text="row.asin" class="asin-cell" />
|
||
</template>
|
||
<span v-else class="asin-empty">-</span>
|
||
</td>
|
||
<td>{{ asinCountryLabel(row.country || '') }}</td>
|
||
<template v-if="row.isFirst">
|
||
<td :rowspan="row.rowspan" class="asin-col-actions">
|
||
<button class="btn btn-sm" type="button" @click="openConfig(row.item as QueryAsinItem)">配置</button>
|
||
</td>
|
||
</template>
|
||
</tr>
|
||
</template>
|
||
<tr v-else-if="loading">
|
||
<td colspan="6" class="empty-tip">加载中...</td>
|
||
</tr>
|
||
<tr v-else>
|
||
<td colspan="6" class="empty-tip">暂无数据</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<OldPagination :total="total" :page="page" :page-size="pageSize" @change="changePage" @size-change="changeSize" />
|
||
</section>
|
||
|
||
<el-dialog v-model="createVisible" title="新增查询 ASIN" width="560px" :close-on-click-modal="false">
|
||
<el-form label-position="top" @submit.prevent>
|
||
<el-form-item label="分组">
|
||
<el-select v-model="createGroupId" placeholder="请选择分组" filterable clearable style="width: 100%" @change="onCreateGroupChange">
|
||
<el-option v-for="group in groups" :key="group.id" :label="group.groupName" :value="group.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="店铺名">
|
||
<el-select
|
||
v-model="createShopName"
|
||
:disabled="createGroupId == null"
|
||
:loading="shopNamesLoading"
|
||
:placeholder="createGroupId == null ? '请先选择分组' : '请选择店铺'"
|
||
filterable
|
||
style="width: 100%"
|
||
>
|
||
<el-option v-for="name in shopNames" :key="name" :label="name" :value="name" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="国家">
|
||
<el-select filterable v-model="createCountry" placeholder="请选择国家" clearable style="width: 100%">
|
||
<el-option v-for="code in QUERY_ASIN_COUNTRIES" :key="code" :label="asinCountryLabel(code)" :value="code" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="ASIN">
|
||
<el-input ref="createAsinInputRef" v-model="createAsin" placeholder="请输入 ASIN" @input="onCreateAsinInput" />
|
||
</el-form-item>
|
||
<el-alert v-if="createMsg" :title="createMsg" :type="createMsgOk ? 'success' : 'error'" :closable="false" show-icon />
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="createVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="creating" @click="submitCreate">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-drawer v-model="drawerVisible" :title="drawerItem ? `${drawerItem.shopName} · ASIN 配置` : 'ASIN 配置'" size="min(520px, 92%)">
|
||
<p class="drawer-tip">留空表示删除该站点已有的 ASIN 与记录。</p>
|
||
<el-form label-position="top">
|
||
<el-form-item v-for="code in QUERY_ASIN_COUNTRIES" :key="code" :label="`${asinCountryLabel(code)} ASIN`">
|
||
<el-input
|
||
v-model="draftAsin[code]"
|
||
:placeholder="asinOf(drawerItem as QueryAsinItem, code) ? '留空删除该站点 ASIN' : '输入 ASIN'"
|
||
@input="normalizeAsinInput(code)"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="drawerVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="drawerSaving" @click="saveConfig">保存</el-button>
|
||
</template>
|
||
</el-drawer>
|
||
|
||
<el-dialog v-model="importVisible" :title="importMode === 'add' ? '导入添加查询 ASIN' : '删除导入查询 ASIN'" width="520px">
|
||
<el-form label-position="top">
|
||
<el-form-item label="分组(Excel 未提供时的兜底,可选)">
|
||
<el-select filterable v-model="importGroupId" placeholder="可不选" clearable style="width: 100%">
|
||
<el-option v-for="group in groups" :key="group.id" :label="group.groupName" :value="group.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="Excel 文件">
|
||
<input type="file" accept=".xlsx,.xls" :disabled="importRunning" @change="onPickImportFile" />
|
||
<p class="import-tip">按 Excel 中的分组、店铺名与国家 ASIN 批量{{ importMode === 'add' ? '添加' : '删除' }},仅支持 .xlsx/.xls。</p>
|
||
</el-form-item>
|
||
<el-alert v-if="importProgress" :title="importProgress" type="info" :closable="false" show-icon />
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button :disabled="importRunning" @click="importVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="importRunning" @click="submitImport">
|
||
{{ importMode === 'add' ? '开始导入' : '开始删除导入' }}
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* 像素复刻旧版 admin.html panel-query-asin(蓝白末层)。 */
|
||
.query-view {
|
||
font-family: inherit;
|
||
color: #24384d;
|
||
}
|
||
.panel-box {
|
||
width: 100%;
|
||
min-width: 0;
|
||
padding: 20px 22px 24px;
|
||
border: 1px solid #d8e3ee;
|
||
border-radius: 14px;
|
||
background: linear-gradient(145deg, #ffffff, #f9fbfd);
|
||
box-shadow: 0 1px 2px rgba(39, 67, 94, 0.04), 0 14px 30px -24px rgba(39, 67, 94, 0.28);
|
||
}
|
||
h3 {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
font-weight: 650;
|
||
color: #24384d;
|
||
letter-spacing: 0.2px;
|
||
}
|
||
.query-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.query-head-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
.form-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: flex-end;
|
||
gap: 14px 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.query-filter-row > .form-group {
|
||
flex: 1 1 140px;
|
||
min-width: 0;
|
||
}
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 7px;
|
||
margin-bottom: 0;
|
||
}
|
||
.form-group label {
|
||
color: #5b6f83;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
}
|
||
.form-group input,
|
||
.form-group select {
|
||
min-width: 0;
|
||
min-height: 42px;
|
||
padding: 8px 12px;
|
||
border: 1px solid #cbd9e6;
|
||
border-radius: 9px;
|
||
background: #f8fbfd;
|
||
color: #24384d;
|
||
font-size: 13.5px;
|
||
font-family: inherit;
|
||
color-scheme: light;
|
||
outline: none;
|
||
transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
|
||
}
|
||
.form-group input:hover,
|
||
.form-group select:hover {
|
||
border-color: #9fb7cd;
|
||
}
|
||
.form-group input:focus,
|
||
.form-group select:focus {
|
||
background: #ffffff;
|
||
border-color: #5f85ad;
|
||
box-shadow: 0 0 0 3px rgba(95, 133, 173, 0.16);
|
||
}
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
min-height: 42px;
|
||
padding: 9px 18px;
|
||
border: 1px solid #4f78a5;
|
||
border-radius: 9px;
|
||
background: linear-gradient(135deg, #5f85ad, #4f78a5);
|
||
color: #ffffff;
|
||
font-family: inherit;
|
||
font-size: 13.5px;
|
||
cursor: pointer;
|
||
box-shadow: 0 8px 18px -14px rgba(79, 120, 165, 0.72);
|
||
transition: background 160ms ease, box-shadow 160ms ease, transform 120ms ease;
|
||
}
|
||
.btn:hover:not(:disabled) {
|
||
background: linear-gradient(135deg, #7094ba, #5d83ac);
|
||
box-shadow: 0 11px 22px -14px rgba(79, 120, 165, 0.8);
|
||
}
|
||
.btn:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.55;
|
||
}
|
||
.btn-secondary {
|
||
background: #ffffff;
|
||
color: #5b6f83;
|
||
border-color: #c7d7e5;
|
||
}
|
||
.btn-secondary:hover:not(:disabled) {
|
||
color: #2f5d8b;
|
||
border-color: #95b1cb;
|
||
background: #edf5fb;
|
||
}
|
||
.btn-danger {
|
||
background: linear-gradient(135deg, #c06d77, #b35f6a);
|
||
border-color: #b35f6a;
|
||
}
|
||
.btn-danger:hover:not(:disabled) {
|
||
background: linear-gradient(135deg, #cb7c84, #b96570);
|
||
}
|
||
.btn-sm {
|
||
min-height: 36px;
|
||
padding: 7px 12px;
|
||
}
|
||
.table-scroll {
|
||
width: 100%;
|
||
min-width: 0;
|
||
overflow-x: auto;
|
||
border: 1px solid #dbe5ee;
|
||
border-radius: 10px;
|
||
background: #ffffff;
|
||
}
|
||
.table-scroll table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
table-layout: fixed;
|
||
}
|
||
.query-asin-table-scroll > table {
|
||
min-width: 860px;
|
||
}
|
||
.table-scroll th,
|
||
.table-scroll td {
|
||
padding: 10px 12px;
|
||
text-align: left;
|
||
font-size: 13.5px;
|
||
line-height: 1.5;
|
||
border-bottom: 1px solid #e0e8ef;
|
||
vertical-align: middle;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.table-scroll th {
|
||
background: #edf4fa;
|
||
color: #4e6479;
|
||
border-bottom-color: #d5e1eb;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.4px;
|
||
}
|
||
.table-scroll tbody tr:hover td {
|
||
background: #f1f7fb;
|
||
}
|
||
.table-scroll tbody tr:last-child td {
|
||
border-bottom: 0;
|
||
}
|
||
.table-scroll td {
|
||
vertical-align: top;
|
||
}
|
||
.asin-cell {
|
||
display: block;
|
||
max-width: 100%;
|
||
margin: -3px 0 -3px -7px;
|
||
padding: 3px 7px;
|
||
border: 1px solid transparent;
|
||
border-radius: 9px;
|
||
background: none;
|
||
color: inherit;
|
||
font-size: 13.5px;
|
||
font-variant-numeric: tabular-nums;
|
||
line-height: 1.5;
|
||
text-align: left;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
cursor: pointer;
|
||
}
|
||
.asin-cell:hover {
|
||
border-color: #c7d7e5;
|
||
background: #eef4fa;
|
||
color: #2f5d8b;
|
||
}
|
||
.asin-empty {
|
||
color: #8b9aaa;
|
||
}
|
||
.asin-col-actions {
|
||
text-align: right;
|
||
background: #ffffff;
|
||
box-shadow: -9px 0 12px -10px rgba(31, 48, 66, 0.45);
|
||
}
|
||
.empty-tip {
|
||
padding: 44px 24px;
|
||
text-align: center;
|
||
color: #8293a5;
|
||
font-size: 13.5px;
|
||
}
|
||
.pagination {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-top: 18px;
|
||
color: #5b6f83;
|
||
font-size: 13px;
|
||
}
|
||
.pagination button {
|
||
min-height: 30px;
|
||
padding: 4px 12px;
|
||
border: 1px solid #c7d7e5;
|
||
border-radius: 8px;
|
||
background: #ffffff;
|
||
color: #5b6f83;
|
||
font-family: inherit;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
}
|
||
.pagination button:hover:not(:disabled) {
|
||
background: #edf5fb;
|
||
border-color: #95b1cb;
|
||
color: #2f5d8b;
|
||
}
|
||
.pagination button:disabled {
|
||
background: #eef3f7;
|
||
color: #9baaba;
|
||
cursor: not-allowed;
|
||
}
|
||
.page-total {
|
||
margin-right: 4px;
|
||
}
|
||
.page-jump {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.page-jump input {
|
||
width: 56px;
|
||
min-height: 30px;
|
||
padding: 4px 8px;
|
||
border: 1px solid #cbd9e6;
|
||
border-radius: 8px;
|
||
background: #f8fbfd;
|
||
color: #24384d;
|
||
font-size: 13px;
|
||
font-family: inherit;
|
||
}
|
||
.drawer-tip {
|
||
color: #5b6f83;
|
||
font-size: 12px;
|
||
margin: 0 0 12px;
|
||
}
|
||
.import-tip {
|
||
margin: 6px 0 0;
|
||
color: #5b6f83;
|
||
font-size: 12px;
|
||
}
|
||
</style>
|