- 移除多余的接口定义文件,简化依赖关系- 更新控制器和服务实现类的注入方式-优化请求参数处理逻辑 - 统一响应数据结构格式- 调整方法签名以提高一致性 - 删除冗余注释和无用代码- 修改系统API调用路径引用位置 - 简化认证服务实现并移除不必要的抽象层 - 优化Excel文件解析相关功能 - 清理无用的工具类和配置项 - 调整错误上报机制的依赖注入方式 - 更新跟卖精灵服务的实现细节- 优化HTTP请求工具函数结构 - 移除废弃的缓存管理服务接口定义 - 调整设备配额检查逻辑复用性 - 优化订单服务的数据返回格式 - 更新产品服务中的数据处理方式 - 重构客户端账户控制器中的设备限制检查逻辑
666 lines
28 KiB
Vue
666 lines
28 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, onMounted, defineAsyncComponent, inject } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { zebraApi, type ZebraOrder, type BanmaAccount } from '../../api/zebra'
|
||
import AccountManager from '../common/AccountManager.vue'
|
||
import { batchConvertImages } from '../../utils/imageProxy'
|
||
import { handlePlatformFileExport } from '../../utils/settings'
|
||
import { getUsernameFromToken } from '../../utils/token'
|
||
|
||
const TrialExpiredDialog = defineAsyncComponent(() => import('../common/TrialExpiredDialog.vue'))
|
||
|
||
const refreshVipStatus = inject<() => Promise<boolean>>('refreshVipStatus')
|
||
|
||
// 接收VIP状态
|
||
const props = defineProps<{
|
||
isVip: boolean
|
||
}>()
|
||
|
||
type Shop = { id: string; shopName: string }
|
||
|
||
const accounts = ref<BanmaAccount[]>([])
|
||
const accountId = ref<number>()
|
||
// 收起功能移除
|
||
|
||
const shopList = ref<Shop[]>([])
|
||
const selectedShops = ref<string[]>([])
|
||
const dateRange = ref<string[]>([])
|
||
|
||
const loading = ref(false)
|
||
const exportLoading = ref(false)
|
||
const progressPercentage = ref(0)
|
||
const showProgress = ref(false)
|
||
const allOrderData = ref<ZebraOrder[]>([])
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(15)
|
||
const currentBatchId = ref('')
|
||
|
||
// 批量获取状态
|
||
const fetchCurrentPage = ref(1)
|
||
const fetchTotalPages = ref(0)
|
||
const fetchTotalItems = ref(0)
|
||
const isFetching = ref(false)
|
||
let abortController: AbortController | null = null
|
||
|
||
// 试用期过期弹框
|
||
const showTrialExpiredDialog = ref(false)
|
||
const trialExpiredType = ref<'device' | 'account' | 'both'>('account')
|
||
|
||
const checkExpiredType = inject<() => 'device' | 'account' | 'both'>('checkExpiredType')
|
||
function selectAccount(id: number) {
|
||
accountId.value = id
|
||
loadShops()
|
||
}
|
||
|
||
const paginatedData = computed(() => {
|
||
const start = (currentPage.value - 1) * pageSize.value
|
||
const end = start + pageSize.value
|
||
return allOrderData.value.slice(start, end)
|
||
})
|
||
|
||
function formatJpy(v?: number) {
|
||
const n = Number(v || 0)
|
||
return `¥${n.toLocaleString('ja-JP')}`
|
||
}
|
||
|
||
function formatCny(v?: number) {
|
||
const n = Number(v || 0)
|
||
return `¥${n.toLocaleString('zh-CN')}`
|
||
}
|
||
|
||
async function loadShops() {
|
||
try {
|
||
const resp = await zebraApi.getShops({ accountId: Number(accountId.value) || undefined })
|
||
const list = (resp as any)?.data?.data?.list ?? (resp as any)?.list ?? []
|
||
shopList.value = list
|
||
} catch (e) {
|
||
console.error('获取店铺列表失败:', e)
|
||
}
|
||
}
|
||
|
||
async function loadAccounts() {
|
||
try {
|
||
const username = getUsernameFromToken()
|
||
const res = await zebraApi.getAccounts(username)
|
||
const list = (res as any)?.data ?? res
|
||
accounts.value = Array.isArray(list) ? list : []
|
||
const def = accounts.value.find(a => a.isDefault === 1) || accounts.value[0]
|
||
accountId.value = def?.id
|
||
await loadShops()
|
||
} catch (e) {
|
||
accounts.value = []
|
||
}
|
||
}
|
||
|
||
function handleSizeChange(size: number) {
|
||
pageSize.value = size
|
||
currentPage.value = 1
|
||
}
|
||
|
||
function handleCurrentChange(page: number) {
|
||
currentPage.value = page
|
||
}
|
||
|
||
async function fetchData() {
|
||
if (isFetching.value) return
|
||
|
||
// 刷新VIP状态
|
||
if (refreshVipStatus) await refreshVipStatus()
|
||
|
||
// VIP检查
|
||
if (!props.isVip) {
|
||
if (checkExpiredType) trialExpiredType.value = checkExpiredType()
|
||
showTrialExpiredDialog.value = true
|
||
return
|
||
}
|
||
|
||
abortController = new AbortController()
|
||
loading.value = true
|
||
isFetching.value = true
|
||
showProgress.value = true
|
||
progressPercentage.value = 0
|
||
allOrderData.value = []
|
||
fetchCurrentPage.value = 1
|
||
fetchTotalItems.value = 0
|
||
currentBatchId.value = `ZEBRA_${Date.now()}`
|
||
|
||
const [startDate = '', endDate = ''] = dateRange.value || []
|
||
await fetchPageData(startDate, endDate)
|
||
}
|
||
|
||
async function fetchPageData(startDate: string, endDate: string) {
|
||
if (!isFetching.value) return
|
||
|
||
try {
|
||
const response = await zebraApi.getOrders({
|
||
accountId: Number(accountId.value) || undefined,
|
||
startDate,
|
||
endDate,
|
||
page: fetchCurrentPage.value,
|
||
pageSize: 50,
|
||
shopIds: selectedShops.value.join(','),
|
||
batchId: currentBatchId.value
|
||
}, abortController?.signal)
|
||
|
||
const data = (response as any)?.data || response
|
||
const orders = data.orders || []
|
||
allOrderData.value = [...allOrderData.value, ...orders]
|
||
|
||
fetchTotalPages.value = data.totalPages || 0
|
||
fetchTotalItems.value = data.total || 0
|
||
|
||
if (fetchCurrentPage.value < fetchTotalPages.value && isFetching.value) {
|
||
progressPercentage.value = Math.round((fetchCurrentPage.value / fetchTotalPages.value) * 100)
|
||
fetchCurrentPage.value++
|
||
setTimeout(() => fetchPageData(startDate, endDate), 200)
|
||
} else {
|
||
progressPercentage.value = 100
|
||
finishFetching()
|
||
}
|
||
} catch (e: any) {
|
||
if (e.name !== 'AbortError') {
|
||
console.error('获取订单数据失败:', e)
|
||
}
|
||
finishFetching()
|
||
}
|
||
}
|
||
|
||
function finishFetching() {
|
||
isFetching.value = false
|
||
loading.value = false
|
||
abortController = null
|
||
// 确保进度条完全填满
|
||
progressPercentage.value = 100
|
||
currentPage.value = 1
|
||
// 进度条保留显示,不自动隐藏
|
||
}
|
||
|
||
function stopFetch() {
|
||
abortController?.abort()
|
||
abortController = null
|
||
isFetching.value = false
|
||
loading.value = false
|
||
// 进度条保留显示,不自动隐藏
|
||
}
|
||
|
||
|
||
function showMessage(message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') {
|
||
ElMessage({ message, type })
|
||
}
|
||
|
||
async function exportToExcel() {
|
||
if (!allOrderData.value.length) {
|
||
showMessage('没有数据可供导出', 'warning')
|
||
return
|
||
}
|
||
|
||
exportLoading.value = true
|
||
|
||
try {
|
||
const ExcelJS = (await import('exceljs')).default
|
||
const workbook = new ExcelJS.Workbook()
|
||
const worksheet = workbook.addWorksheet('斑马订单数据')
|
||
|
||
worksheet.columns = [
|
||
{ header: '下单时间', key: 'orderedAt', width: 15 },
|
||
{ header: '商品图片', key: 'image', width: 15 },
|
||
{ header: '商品名称', key: 'productTitle', width: 25 },
|
||
{ header: '乐天订单号', key: 'shopOrderNumber', width: 20 },
|
||
{ header: '下单距今', key: 'timeSinceOrder', width: 12 },
|
||
{ header: '订单金额/日元', key: 'priceJpy', width: 15 },
|
||
{ header: '数量', key: 'productQuantity', width: 8 },
|
||
{ header: '税费/日元', key: 'shippingFeeJpy', width: 12 },
|
||
{ header: '回款抽点rmb', key: 'serviceFee', width: 15 },
|
||
{ header: '商品番号', key: 'productNumber', width: 15 },
|
||
{ header: '1688订单号', key: 'poNumber', width: 15 },
|
||
{ header: '采购金额/rmb', key: 'shippingFeeCny', width: 15 },
|
||
{ header: '国际运费/rmb', key: 'internationalShippingFee', width: 15 },
|
||
{ header: '国内物流', key: 'poLogisticsCompany', width: 12 },
|
||
{ header: '国内单号', key: 'poTrackingNumber', width: 15 },
|
||
{ header: '日本单号', key: 'internationalTrackingNumber', width: 15 },
|
||
{ header: '地址状态', key: 'trackInfo', width: 12 }
|
||
]
|
||
|
||
const imageUrls = allOrderData.value.map(order => order.productImage || '')
|
||
const imageBase64s = await batchConvertImages(imageUrls, 80)
|
||
|
||
for (let i = 0; i < allOrderData.value.length; i++) {
|
||
const order = allOrderData.value[i]
|
||
const base64Image = imageBase64s[i]
|
||
|
||
const row = worksheet.addRow({
|
||
orderedAt: order.orderedAt || '',
|
||
image: base64Image ? '图片' : '无图片',
|
||
productTitle: order.productTitle || '',
|
||
shopOrderNumber: order.shopOrderNumber || '',
|
||
timeSinceOrder: order.timeSinceOrder || '',
|
||
priceJpy: formatJpy(order.priceJpy),
|
||
productQuantity: order.productQuantity || 0,
|
||
shippingFeeJpy: formatJpy(order.shippingFeeJpy),
|
||
serviceFee: order.serviceFee || '',
|
||
productNumber: order.productNumber || '',
|
||
poNumber: order.poNumber || '',
|
||
shippingFeeCny: formatCny(order.shippingFeeCny),
|
||
internationalShippingFee: order.internationalShippingFee || '',
|
||
poLogisticsCompany: order.poLogisticsCompany || '',
|
||
poTrackingNumber: order.poTrackingNumber || '',
|
||
internationalTrackingNumber: order.internationalTrackingNumber || '',
|
||
trackInfo: order.trackInfo || ''
|
||
})
|
||
|
||
if (base64Image) {
|
||
const base64Data = base64Image.split(',')[1]
|
||
if (base64Data) {
|
||
const imageId = workbook.addImage({
|
||
base64: base64Data,
|
||
extension: 'jpeg',
|
||
})
|
||
|
||
worksheet.addImage(imageId, {
|
||
tl: { col: 1, row: row.number - 1 },
|
||
ext: { width: 60, height: 60 }
|
||
})
|
||
|
||
row.height = 50
|
||
}
|
||
}
|
||
}
|
||
|
||
const buffer = await workbook.xlsx.writeBuffer()
|
||
const blob = new Blob([buffer], {
|
||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||
})
|
||
const fileName = `斑马订单数据_${new Date().toISOString().slice(0, 10)}.xlsx`
|
||
|
||
const success = await handlePlatformFileExport('zebra', blob, fileName)
|
||
|
||
if (success) {
|
||
showMessage('Excel文件导出成功!', 'success')
|
||
}
|
||
} catch (error) {
|
||
showMessage('导出失败', 'error')
|
||
} finally {
|
||
exportLoading.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadAccounts()
|
||
try {
|
||
const latest = await zebraApi.getLatestOrders()
|
||
const data = (latest as any)?.data || latest
|
||
allOrderData.value = data?.orders || []
|
||
} catch {}
|
||
})
|
||
|
||
// 账号对话框
|
||
const accountDialogVisible = ref(false)
|
||
const accountForm = ref<BanmaAccount>({ isDefault: 0, status: 1 })
|
||
const isEditMode = ref(false)
|
||
const formUsername = ref('')
|
||
const formPassword = ref('')
|
||
const rememberPwd = ref(true)
|
||
const managerVisible = ref(false)
|
||
const accountManagerRef = ref()
|
||
|
||
function openAddAccount() {
|
||
isEditMode.value = false
|
||
accountForm.value = { name: '', username: '', isDefault: 0, status: 1 }
|
||
formUsername.value = ''
|
||
formPassword.value = ''
|
||
accountDialogVisible.value = true
|
||
}
|
||
|
||
function openManageAccount() {
|
||
const cur = accounts.value.find(a => a.id === accountId.value)
|
||
if (!cur) return
|
||
isEditMode.value = true
|
||
accountForm.value = { ...cur }
|
||
formUsername.value = cur.username || ''
|
||
formPassword.value = localStorage.getItem(`banma:pwd:${cur.username || ''}`) || ''
|
||
accountDialogVisible.value = true
|
||
}
|
||
|
||
async function submitAccount() {
|
||
if (!formUsername.value) { ElMessage({ message: '请输入账号', type: 'warning' }); return }
|
||
const payload: BanmaAccount = {
|
||
id: accountForm.value.id,
|
||
name: accountForm.value.name || formUsername.value,
|
||
username: formUsername.value,
|
||
password: formPassword.value || '',
|
||
isDefault: accountForm.value.isDefault || 0,
|
||
status: accountForm.value.status || 1,
|
||
}
|
||
try {
|
||
const username = getUsernameFromToken()
|
||
const res = await zebraApi.saveAccount(payload, username)
|
||
const id = (res as any)?.data?.id || (res as any)?.id
|
||
if (!id) throw new Error((res as any)?.msg || '保存失败')
|
||
accountDialogVisible.value = false
|
||
await loadAccounts()
|
||
if (id) accountId.value = id
|
||
if (managerVisible.value && accountManagerRef.value?.load) {
|
||
accountManagerRef.value.load()
|
||
}
|
||
} catch (e: any) {
|
||
ElMessage({ message: e?.message || '账号或密码错误,无法获取Token', type: 'error' })
|
||
}
|
||
}
|
||
|
||
async function removeCurrentAccount() {
|
||
if (!isEditMode.value || !accountForm.value.id) return
|
||
try {
|
||
await ElMessageBox.confirm('确认删除该账号?', '提示', { type: 'warning' })
|
||
} catch { return }
|
||
await zebraApi.removeAccount(accountForm.value.id)
|
||
accountDialogVisible.value = false
|
||
await loadAccounts()
|
||
}
|
||
</script>
|
||
<template>
|
||
<div class="zebra-root">
|
||
<div class="layout">
|
||
<aside class="aside">
|
||
<div class="aside-header">
|
||
<span>操作流程</span>
|
||
</div>
|
||
<div class="aside-steps">
|
||
<section class="step step-accounts">
|
||
<div class="step-index">1</div>
|
||
<div class="step-body">
|
||
<div class="step-title">需要查询的账号</div>
|
||
<div class="tip">请选择需要查询数据的账号,如未添加账号,请点击“添加账号”。</div>
|
||
<template v-if="accounts.length">
|
||
<el-scrollbar :class="['account-list', { 'scroll-limit': accounts.length > 3 }]">
|
||
<div>
|
||
<div
|
||
v-for="a in accounts"
|
||
:key="a.id"
|
||
:class="['acct-item', { selected: accountId === a.id }]"
|
||
@click="selectAccount(Number(a.id))"
|
||
>
|
||
<span class="acct-row">
|
||
<span :class="['status-dot', a.status === 1 ? 'on' : 'off']"></span>
|
||
<img class="avatar" src="/image/img_v3_02qd_052605f0-4be3-44db-9691-35ee5ff6201g.jpg" alt="avatar" />
|
||
<span class="acct-text">{{ a.name || a.username }}</span>
|
||
<span v-if="accountId === a.id" class="acct-check">✔️</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</el-scrollbar>
|
||
</template>
|
||
<template v-else>
|
||
<div class="placeholder-box">
|
||
<img class="placeholder-img" src="/icon/image.png" alt="add-account" />
|
||
<div class="placeholder-tip">请添加 斑马ERP 账号</div>
|
||
</div>
|
||
</template>
|
||
<div class="step-actions btn-row sticky-actions">
|
||
<el-button size="small" class="w50" @click="openAddAccount">添加账号</el-button>
|
||
<el-button size="small" class="w50 btn-blue" @click="managerVisible = true">账号管理</el-button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="step">
|
||
<div class="step-index">2</div>
|
||
<div class="step-body">
|
||
<div class="step-title">需要查询的日期</div>
|
||
<div class="tip">请选择查询数据的日期范围。</div>
|
||
<el-select v-model="selectedShops" multiple placeholder="选择店铺" :disabled="loading || !accounts.length" size="small" style="width: 100%">
|
||
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.shopName" :value="shop.id" />
|
||
</el-select>
|
||
<div style="height: 8px"></div>
|
||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" :disabled="loading || !accounts.length" size="small" style="width: 100%" />
|
||
</div>
|
||
</section>
|
||
|
||
<section class="step">
|
||
<div class="step-index">3</div>
|
||
<div class="step-body">
|
||
<div class="step-title">获取数据</div>
|
||
<div class="tip">点击下方按钮,开始查询订单数据。</div>
|
||
<div class="btn-col">
|
||
<el-button size="small" class="w100 btn-blue" :disabled="loading || !accounts.length" @click="fetchData">{{ loading ? '处理中...' : '获取数据' }}</el-button>
|
||
<el-button size="small" :disabled="!loading" @click="stopFetch" class="w100">停止获取</el-button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="step">
|
||
<div class="step-index">4</div>
|
||
<div class="step-body">
|
||
<div class="step-title">导出数据</div>
|
||
<div class="tip">点击下方按钮导出所有订单数据到 Excel 文件</div>
|
||
<div class="btn-col">
|
||
<el-button size="small" type="success" :disabled="exportLoading || !allOrderData.length" :loading="exportLoading" @click="exportToExcel" class="w100">{{ exportLoading ? '导出中...' : '导出数据' }}</el-button>
|
||
<!-- 导出进度条 -->
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</aside>
|
||
|
||
<div class="content">
|
||
<!-- 数据表格(无数据时也显示表头) -->
|
||
<div class="table-section">
|
||
<div v-if="showProgress" class="progress-section">
|
||
<div class="progress-box">
|
||
<div class="progress-container">
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" :style="{ width: progressPercentage + '%' }"></div>
|
||
</div>
|
||
<div class="progress-text">{{ progressPercentage }}%</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>下单时间</th>
|
||
<th>商品图片</th>
|
||
<th>商品名称</th>
|
||
<th>乐天订单号</th>
|
||
<th>下单距今</th>
|
||
<th>订单金额/日元</th>
|
||
<th>数量</th>
|
||
<th>税费/日元</th>
|
||
<th>回款抽点rmb</th>
|
||
<th>商品番号</th>
|
||
<th>1688订单号</th>
|
||
<th>采购金额/rmb</th>
|
||
<th>国际运费/rmb</th>
|
||
<th>国内物流</th>
|
||
<th>国内单号</th>
|
||
<th>日本单号</th>
|
||
<th>地址状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in paginatedData" :key="row.shopOrderNumber + (row.productNumber || '')">
|
||
<td>{{ row.orderedAt || '-' }}</td>
|
||
<td>
|
||
<div class="image-container" v-if="row.productImage">
|
||
<el-image :src="row.productImage" class="thumb" fit="contain" :preview-src-list="[row.productImage]" />
|
||
</div>
|
||
<span v-else>无图片</span>
|
||
</td>
|
||
<td class="truncate" :title="row.productTitle">{{ row.productTitle }}</td>
|
||
<td class="truncate" :title="row.shopOrderNumber">{{ row.shopOrderNumber }}</td>
|
||
<td>{{ row.timeSinceOrder || '-' }}</td>
|
||
<td><span class="price-tag">{{ formatJpy(row.priceJpy) }}</span></td>
|
||
<td>{{ row.productQuantity || 0 }}</td>
|
||
<td><span class="fee-tag">{{ formatJpy(row.shippingFeeJpy) }}</span></td>
|
||
<td>{{ row.serviceFee || '-' }}</td>
|
||
<td class="truncate" :title="row.productNumber">{{ row.productNumber }}</td>
|
||
<td class="truncate" :title="row.poNumber">{{ row.poNumber }}</td>
|
||
<td><span class="fee-tag">{{ formatCny(row.shippingFeeCny) }}</span></td>
|
||
<td>{{ row.internationalShippingFee || '-' }}</td>
|
||
<td>{{ row.poLogisticsCompany || '-' }}</td>
|
||
<td class="truncate" :title="row.poTrackingNumber">{{ row.poTrackingNumber }}</td>
|
||
<td class="truncate" :title="row.internationalTrackingNumber">{{ row.internationalTrackingNumber }}</td>
|
||
<td>
|
||
<span v-if="row.trackInfo" class="tag">{{ row.trackInfo }}</span>
|
||
<span v-else>暂无</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-if="paginatedData.length === 0" class="empty-abs">
|
||
<div v-if="loading" class="empty-container">
|
||
<div class="spinner">⟳</div>
|
||
<div>加载中...</div>
|
||
</div>
|
||
<div v-else class="empty-container">
|
||
<div class="empty-icon" style="font-size:48px;">📄</div>
|
||
<div class="empty-text">暂无数据,请获取订单</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部区域:分页器 -->
|
||
<div class="pagination-fixed">
|
||
<el-pagination
|
||
background
|
||
:current-page="currentPage"
|
||
:page-sizes="[15,30,50,100]"
|
||
:page-size="pageSize"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:total="allOrderData.length"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 账号新增/编辑对话框 -->
|
||
<el-dialog v-model="accountDialogVisible" width="420px" class="add-account-dialog">
|
||
<template #header>
|
||
<div class="aad-header">
|
||
<img class="aad-icon" src="/icon/image.png" alt="logo" />
|
||
<div class="aad-title">添加账号</div>
|
||
</div>
|
||
</template>
|
||
<div class="aad-row">
|
||
<el-input v-model="formUsername" placeholder="请输入账号" />
|
||
</div>
|
||
<div class="aad-row">
|
||
<el-input v-model="formPassword" placeholder="请输入密码" type="password" show-password />
|
||
</div>
|
||
<div class="aad-row aad-opts">
|
||
<el-checkbox v-model="rememberPwd">保存密码</el-checkbox>
|
||
</div>
|
||
<template #footer>
|
||
<el-button type="primary" class="btn-blue" style="width: 100%" @click="submitAccount">登录</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 试用期过期弹框 -->
|
||
<TrialExpiredDialog v-model="showTrialExpiredDialog" :expired-type="trialExpiredType" />
|
||
|
||
<AccountManager ref="accountManagerRef" v-model="managerVisible" platform="zebra" @add="openAddAccount" @refresh="loadAccounts" />
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
export default {
|
||
name: 'ZebraDashboard',
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.zebra-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
|
||
.layout { background: #fff; border-radius: 4px; padding: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: grid; grid-template-columns: 220px 1fr; gap: 12px; }
|
||
.aside { border: 1px solid #ebeef5; border-radius: 4px; padding: 10px; display: flex; flex-direction: column; transition: width 0.2s ease; }
|
||
.aside.collapsed { width: 56px; overflow: hidden; }
|
||
.aside-header { display: flex; justify-content: flex-start; align-items: center; font-weight: 600; color: #606266; margin-bottom: 8px; }
|
||
.aside-steps { position: relative; }
|
||
.step { display: grid; grid-template-columns: 22px 1fr; gap: 10px; position: relative; padding: 8px 0; }
|
||
.step + .step { border-top: 1px dashed #ebeef5; }
|
||
.step-index { width: 22px; height: 22px; background: #1677FF; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; margin-top: 2px; }
|
||
.step-body { min-width: 0; text-align: left; }
|
||
.step-title { font-size: 13px; color: #606266; margin-bottom: 6px; font-weight: 600; text-align: left; }
|
||
.aside-steps:before { content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6); }
|
||
.account-list {height: auto; }
|
||
.step-actions { margin-top: 8px; display: flex; gap: 8px; }
|
||
.step-accounts { position: relative; }
|
||
.sticky-actions { position: sticky; bottom: 0; background: #fafafa; padding-top: 8px; }
|
||
.scroll-limit { max-height: 160px; }
|
||
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||
.btn-col { display: flex; flex-direction: column; gap: 6px; }
|
||
.w50 { width: 48%; }
|
||
.w100 { width: 100%; }
|
||
.placeholder-box { display:flex; align-items:center; justify-content:center; flex-direction:column; height: 140px; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
|
||
.placeholder-img { width: 120px; opacity: 0.9; }
|
||
.placeholder-tip { margin-top: 6px; font-size: 12px; color: #a8abb2; }
|
||
.aside :deep(.el-date-editor) { width: 100%; }
|
||
.aside :deep(.el-range-editor.el-input__wrapper) { width: 100%; box-sizing: border-box; }
|
||
.aside :deep(.el-input),
|
||
.aside :deep(.el-input__wrapper),
|
||
.aside :deep(.el-select) { width: 100%; box-sizing: border-box; }
|
||
.aside :deep(.el-button + .el-button) { margin-left: 0 !important; }
|
||
.btn-row :deep(.el-button) { width: 100%; }
|
||
.btn-col :deep(.el-button) { width: 100%; }
|
||
.btn-blue { background: #1677FF; border-color: #1677FF; color: #fff; }
|
||
.btn-blue:disabled { background: #a6c8ff; border-color: #a6c8ff; color: #fff; }
|
||
.tip { color: #909399; font-size: 12px; margin-bottom: 8px; text-align: left; }
|
||
.avatar { width: 22px; height: 22px; border-radius: 50%; margin-right: 6px; vertical-align: -2px; }
|
||
.acct-text { vertical-align: middle; }
|
||
.acct-row { display: grid; grid-template-columns: 8px 18px 1fr auto; align-items: center; gap: 6px; width: 100%; }
|
||
.acct-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; font-size: 12px; }
|
||
.status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
|
||
.status-dot.on { background: #22c55e; }
|
||
.status-dot.off { background: #f87171; }
|
||
.acct-item { padding: 6px 8px; border-radius: 8px; cursor: pointer; }
|
||
.acct-item.selected { background: #eef5ff; box-shadow: inset 0 0 0 1px #d6e4ff; }
|
||
.acct-check { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 50%; background: transparent; color: #111; font-size: 14px; }
|
||
.account-list::-webkit-scrollbar { width: 0; height: 0; }
|
||
.add-account-dialog .aad-header { display:flex; flex-direction: column; align-items:center; gap:8px; padding-top: 8px; width: 100%; }
|
||
.add-account-dialog .aad-icon { width: 120px; height: auto; }
|
||
.add-account-dialog .aad-title { font-weight: 600; font-size: 18px; text-align: center; }
|
||
.add-account-dialog .aad-row { margin-top: 12px; }
|
||
.add-account-dialog .aad-opts { display:flex; align-items:center; }
|
||
|
||
/* 居中 header,避免右上角关闭按钮影响视觉中心 */
|
||
:deep(.add-account-dialog .el-dialog__header) { text-align: center; padding-right: 0; display: block; }
|
||
.content { display: grid; grid-template-rows: 1fr auto; min-height: 0; }
|
||
.table-section { min-height: 0; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column; }
|
||
.table-wrapper { flex: 1; overflow: auto; overflow-x: auto; }
|
||
.table-wrapper { scrollbar-width: thin; scrollbar-color: #c0c4cc transparent; }
|
||
.table-wrapper::-webkit-scrollbar { width: 6px; height: 6px; }
|
||
.table-wrapper::-webkit-scrollbar-track { background: transparent; }
|
||
.table-wrapper::-webkit-scrollbar-thumb { background: #c0c4cc; border-radius: 3px; }
|
||
.table-wrapper:hover::-webkit-scrollbar-thumb { background: #a8abb2; }
|
||
.table { width: max-content; min-width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.table th { background: #f5f7fa; color: #909399; font-weight: 600; padding: 12px 8px; border-bottom: 2px solid #ebeef5; text-align: left; white-space: nowrap; }
|
||
.table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
|
||
.table tbody tr:hover { background: #f9f9f9; }
|
||
.truncate { max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.image-container { display: flex; justify-content: center; align-items: center; width: 28px; height: 24px; margin: 0 auto; background: #f8f9fa; border-radius: 2px; }
|
||
.thumb { width: 22px; height: 22px; object-fit: contain; border-radius: 2px; }
|
||
.price-tag { color: #e6a23c; font-weight: bold; }
|
||
.fee-tag { color: #909399; font-weight: 500; }
|
||
.table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; }
|
||
.spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; }
|
||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||
.pagination-fixed { position: sticky; bottom: 0; z-index: 2; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; }
|
||
.tag { display: inline-block; padding: 0 6px; margin-left: 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; }
|
||
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; pointer-events: none; }
|
||
.progress-section { margin: 0px 12px 0px 12px; }
|
||
.progress-box { padding: 4px 0; }
|
||
.progress-container { display: flex; align-items: center; gap: 8px; }
|
||
.progress-bar { flex: 1; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden; }
|
||
.progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
|
||
.progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
|
||
.export-progress { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 0 4px; }
|
||
.export-progress-bar { flex: 1; height: 4px; background: #e3eeff; border-radius: 2px; overflow: hidden; }
|
||
.export-progress-fill { height: 100%; background: #67c23a; border-radius: 2px; transition: width 0.3s ease; }
|
||
.export-progress-text { font-size: 11px; color: #67c23a; font-weight: 500; min-width: 32px; text-align: right; }
|
||
</style>
|
||
|
||
|