1
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { zebraApi, type ZebraOrder } from '../../api/zebra'
|
||||
import { zebraApi, type ZebraOrder, type BanmaAccount } from '../../api/zebra'
|
||||
import AccountManager from '../common/AccountManager.vue'
|
||||
|
||||
type Shop = { id: string; shopName: string }
|
||||
|
||||
const accounts = ref<BanmaAccount[]>([])
|
||||
const accountId = ref<number>()
|
||||
const isCollapsed = ref(false)
|
||||
|
||||
const shopList = ref<Shop[]>([])
|
||||
const selectedShops = ref<string[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
@@ -21,6 +26,10 @@ const fetchCurrentPage = ref(1)
|
||||
const fetchTotalPages = ref(0)
|
||||
const fetchTotalItems = ref(0)
|
||||
const isFetching = ref(false)
|
||||
function selectAccount(id: number) {
|
||||
accountId.value = id
|
||||
loadShops()
|
||||
}
|
||||
|
||||
const paginatedData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
@@ -48,6 +57,19 @@ async function loadShops() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAccounts() {
|
||||
try {
|
||||
const res = await zebraApi.getAccounts()
|
||||
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
|
||||
@@ -133,52 +155,154 @@ async function exportToExcel() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadShops()
|
||||
await loadAccounts()
|
||||
try {
|
||||
const latest = await zebraApi.getLatestOrders()
|
||||
allOrderData.value = latest?.orders || []
|
||||
} catch {}
|
||||
})
|
||||
</script>
|
||||
|
||||
// 账号对话框
|
||||
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)
|
||||
|
||||
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) { alert('请输入账号'); return }
|
||||
const payload: BanmaAccount = {
|
||||
id: accountForm.value.id,
|
||||
name: accountForm.value.name || formUsername.value,
|
||||
username: formUsername.value,
|
||||
isDefault: accountForm.value.isDefault || 0,
|
||||
status: accountForm.value.status || 1,
|
||||
}
|
||||
const { id } = await zebraApi.saveAccount(payload)
|
||||
if (rememberPwd.value && formPassword.value) {
|
||||
localStorage.setItem(`banma:pwd:${formUsername.value}`, formPassword.value)
|
||||
} else {
|
||||
localStorage.removeItem(`banma:pwd:${formUsername.value}`)
|
||||
}
|
||||
accountDialogVisible.value = false
|
||||
await loadAccounts()
|
||||
if (id) accountId.value = id
|
||||
}
|
||||
|
||||
async function removeCurrentAccount() {
|
||||
if (!isEditMode.value || !accountForm.value.id) return
|
||||
if (!confirm('确认删除该账号?')) return
|
||||
await zebraApi.removeAccount(accountForm.value.id)
|
||||
accountDialogVisible.value = false
|
||||
await loadAccounts()
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="zebra-root">
|
||||
<div class="main-container">
|
||||
|
||||
<!-- 筛选和操作区域 -->
|
||||
<div class="import-section">
|
||||
<div class="import-controls">
|
||||
<!-- 店铺选择 -->
|
||||
<el-select v-model="selectedShops" multiple placeholder="选择店铺" style="width: 260px;" :disabled="loading">
|
||||
<el-option v-for="shop in shopList" :key="shop.id" :label="shop.shopName" :value="shop.id"></el-option>
|
||||
</el-select>
|
||||
|
||||
<!-- 日期选择 -->
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 200px;"
|
||||
:disabled="loading"
|
||||
/>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" :disabled="loading" @click="fetchData">
|
||||
📂 {{ loading ? '处理中...' : '获取订单数据' }}
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!loading" @click="stopFetch">停止获取</el-button>
|
||||
<el-button type="success" :disabled="exportLoading || !allOrderData.length" @click="exportToExcel">导出Excel</el-button>
|
||||
</div>
|
||||
<div class="layout">
|
||||
<aside :class="['aside', { collapsed: isCollapsed }]">
|
||||
<div class="aside-header">
|
||||
<span>操作流程</span>
|
||||
<el-button link @click="isCollapsed = !isCollapsed">{{ isCollapsed ? '展开' : '收起' }}</el-button>
|
||||
</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="a.isDefault===1" class="tag">默认</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>
|
||||
|
||||
<!-- 进度条显示:移动到底部,以免挤压表头 -->
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- 数据显示区域 -->
|
||||
<div class="table-container">
|
||||
<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" @click="exportToExcel" class="w100">导出数据</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="content">
|
||||
<!-- 数据表格(无数据时也显示表头) -->
|
||||
<div class="table-section">
|
||||
<div class="table-wrapper">
|
||||
@@ -237,17 +361,15 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<div v-if="paginatedData.length === 0" class="empty-abs">
|
||||
<div class="empty-container">
|
||||
<div v-if="loading" class="empty-container">
|
||||
<div class="spinner">⟳</div>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
<div v-else class="empty-container">
|
||||
<div class="empty-icon">📄</div>
|
||||
<div class="empty-text">暂无数据,请获取订单</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格加载遮罩:仅在无数据时显示 -->
|
||||
<div v-if="loading && !allOrderData.length" class="table-loading">
|
||||
<div class="spinner">⟳</div>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部区域:进度条 + 分页器 -->
|
||||
@@ -269,6 +391,29 @@ onMounted(async () => {
|
||||
</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>
|
||||
<AccountManager v-model="managerVisible" platform="zebra" @add="openAddAccount" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -280,26 +425,68 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.zebra-root { position: absolute; inset: 0; background: #f5f5f5; padding: 12px; box-sizing: border-box; }
|
||||
.main-container { background: #fff; border-radius: 4px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); height: 100%; display: flex; flex-direction: column; }
|
||||
.import-section { margin-bottom: 10px; flex-shrink: 0; }
|
||||
.import-controls { display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.action-buttons { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.progress-section { margin: 15px 0 10px 0; }
|
||||
.progress-box { padding: 8px 0; }
|
||||
.progress-container { display: flex; align-items: center; position: relative; padding-right: 50px; margin-bottom: 8px; }
|
||||
.progress-bar { flex: 1; height: 3px; background: #ebeef5; border-radius: 2px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: linear-gradient(90deg, #409EFF, #66b1ff); border-radius: 2px; transition: width 0.3s ease; }
|
||||
.progress-text { position: absolute; right: 0; font-size: 13px; color: #409EFF; font-weight: 500; }
|
||||
.current-status { font-size: 12px; color: #606266; padding-left: 2px; }
|
||||
.table-container { display: flex; flex-direction: column; flex: 1; min-height: 400px; overflow: hidden; }
|
||||
.empty-section { flex: 1; display: flex; justify-content: center; align-items: center; background: #fff; border: 1px solid #ebeef5; border-radius: 6px; }
|
||||
.empty-container { text-align: center; }
|
||||
.empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.6; }
|
||||
.empty-text { font-size: 14px; color: #909399; }
|
||||
.table-section { flex: 1; overflow: hidden; position: relative; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; display: flex; flex-direction: column; }
|
||||
.table-wrapper { flex: 1; overflow: auto; }
|
||||
.table { 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; }
|
||||
.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: space-between; align-items: center; font-weight: 600; color: #606266; margin-bottom: 8px; }
|
||||
.aside-steps { position: relative; }
|
||||
.step { display: grid; grid-template-columns: 24px 1fr; gap: 10px; position: relative; padding: 8px 0; }
|
||||
.step + .step { border-top: 1px dashed #ebeef5; }
|
||||
.step-index { width: 24px; height: 24px; 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: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
|
||||
.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: 18px; height: 18px; 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%; }
|
||||
.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; }
|
||||
@@ -310,14 +497,13 @@ export default {
|
||||
.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 { flex-shrink: 0; 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: 2px 6px; font-size: 12px; background: #ecf5ff; color: #409EFF; border-radius: 3px; }
|
||||
.empty-tip { text-align: center; color: #909399; padding: 16px 0; }
|
||||
.empty-container { text-align: center; }
|
||||
.empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.6; }
|
||||
.empty-text { font-size: 14px; color: #909399; }
|
||||
.empty-abs { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; display: flex; align-items: center; justify-content: center; }
|
||||
.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-bottom { display: flex; align-items: center; gap: 8px; margin-right: auto; }
|
||||
.progress-bottom .progress-bar { width: 100%; height: 6px; background: #e3eeff; border-radius: 999px; overflow: hidden; }
|
||||
.progress-bottom .progress-fill { height: 100%; background: #1677FF; border-radius: 999px; transition: width 0.3s ease; }
|
||||
.progress-bottom .progress-text { font-size: 13px; color: #1677FF; font-weight: 500; min-width: 44px; text-align: right; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user