change store
Some checks failed
Some checks failed
This commit is contained in:
parent
8d080fce47
commit
0543936df8
117
.github/workflows/ci.yaml
vendored
Normal file
117
.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master, develop ]
|
||||
pull_request:
|
||||
branches: [ main, master, develop ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-${{ matrix.node-version }}
|
||||
path: dist/
|
||||
retention-days: 7
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
|
||||
- name: TypeScript type check
|
||||
run: npx tsc --noEmit
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
continue-on-error: true
|
||||
|
||||
deploy-preview:
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist-20.x
|
||||
path: dist/
|
||||
|
||||
- name: Deploy to preview environment
|
||||
run: echo "Preview deployment would happen here"
|
||||
# 可以添加实际的部署步骤,例如部署到 Vercel、Netlify 等
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run npm audit
|
||||
run: npm audit --audit-level=moderate
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run Snyk security scan
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
384
backups/EmployeeAdd.vue.bak
Normal file
384
backups/EmployeeAdd.vue.bak
Normal file
@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<div class="employee-add">
|
||||
<el-card>
|
||||
<template>
|
||||
<div class="card-header">
|
||||
<span>添加代理</span>
|
||||
<el-button @click="$router.push('/admin/employees')">
|
||||
<el-icon>
|
||||
<Back />
|
||||
</el-icon>返回列表
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-steps :active="activeStep" finish-status="success" simple class="steps">
|
||||
<el-step title="填写信息" />
|
||||
<el-step title="确认信息" />
|
||||
<el-step title="完成" />
|
||||
</el-steps>
|
||||
|
||||
<!-- 第一步:填写信息 -->
|
||||
<div v-if="activeStep === 1" class="step-content">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="add-form">
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入代理姓名" :prefix-icon="User" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入密码(至少6位)" :prefix-icon="Lock"
|
||||
show-password clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input v-model="form.confirmPassword" type="password" placeholder="请再次输入密码" :prefix-icon="Lock"
|
||||
show-password clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" :prefix-icon="Lock" maxlength="11" clearable
|
||||
show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="nextStep" :loading="checking">
|
||||
下一步,确认信息
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 工号预览(动态生成) -->
|
||||
<el-card shadow="never" class="preview-card">
|
||||
<template #header>
|
||||
<span>工号预览</span>
|
||||
</template>
|
||||
<div class="preview-content">
|
||||
<div class="preview-item">
|
||||
<span class="label">工号格式:</span>
|
||||
<span class="value">5位数字,自动生成</span>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<span class="label">账号格式:</span>
|
||||
<span class="value">dl_ + 工号(如 dl_00001)</span>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<span class="label">示例工号:</span>
|
||||
<el-tag size="small">{{ previewEmployeeId }}</el-tag>
|
||||
</div>
|
||||
<div class="preview-item">
|
||||
<span class="label">示例账号:</span>
|
||||
<el-tag size="small" type="success">{{ previewUsername }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 第二步:确认信息 -->
|
||||
<div v-if="activeStep === 2" class="step-content">
|
||||
<el-descriptions :column="1" border class="confirm-info">
|
||||
<el-descriptions-item label="姓名">{{ form.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工号">{{ previewEmployeeId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="登录账号">{{ previewUsername }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ form.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="初始积分">0</el-descriptions-item>
|
||||
<el-descriptions-item label="角色">代理</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">正常</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="step-actions">
|
||||
<el-button @click="prevStep">上一步</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="submitting">
|
||||
确认添加
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第三步:完成 -->
|
||||
<div v-if="activeStep === 3" class="step-content result">
|
||||
<el-result icon="success" :title="`添加成功`" :sub-title="`代理 ${resultData?.name} (${resultData?.username}) 已添加`">
|
||||
<template #extra>
|
||||
<div class="result-info" ref="resultInfoRef">
|
||||
<el-alert type="info" :closable="false" show-icon>
|
||||
<p class="title-center">进销存系统</p>
|
||||
<p>登录网址:https://psi.buzhiyushu.cn/</p>
|
||||
<p>工号:{{ resultData?.employee_id }}</p>
|
||||
<p>账号:{{ resultData?.username }}</p>
|
||||
<p>初始密码:{{ form.password }}</p>
|
||||
<p>手机号:{{ form.phone }}</p>
|
||||
<p style="color: #f56c6c; margin-top: 10px;">请妥善保管账号信息!</p>
|
||||
</el-alert>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<el-button type="primary" @click="resetForm">继续添加</el-button>
|
||||
<el-button @click="$router.push('/admin/employees')">查看列表</el-button>
|
||||
<el-button type="success" @click="copyResultInfo">
|
||||
<el-icon>
|
||||
<DocumentCopy />
|
||||
</el-icon>
|
||||
一键复制
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User, Lock, Back, DocumentCopy } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const activeStep = ref(1)
|
||||
const checking = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref(null)
|
||||
const resultData = ref(null)
|
||||
const resultInfoRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
name: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
phone: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const validatePass = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请输入密码'))
|
||||
} else if (value.length < 6) {
|
||||
callback(new Error('密码长度不能小于6位'))
|
||||
} else {
|
||||
if (form.confirmPassword !== '') {
|
||||
formRef.value?.validateField('confirmPassword')
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const validatePass2 = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'))
|
||||
} else if (value !== form.password) {
|
||||
callback(new Error('两次输入密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入代理姓名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '姓名长度应为2-20个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ validator: validatePass, trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ validator: validatePass2, trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 预览工号和账号(模拟)
|
||||
const previewEmployeeId = ref('00001')
|
||||
const previewUsername = computed(() => `dl_${previewEmployeeId.value}`)
|
||||
|
||||
// 获取下一个可用工号
|
||||
const fetchNextEmployeeId = async () => {
|
||||
try {
|
||||
// 这里可以调用接口获取下一个工号
|
||||
// 如果没有接口,使用模拟数据
|
||||
const res = await request.get('/admin/employee/list', { params: { page: 1, page_size: 1 } })
|
||||
if (res.code === 200 && res.data.list.length > 0) {
|
||||
const lastId = res.data.list[0].employee_id
|
||||
const seq = parseInt(lastId) + 1
|
||||
previewEmployeeId.value = String(seq).padStart(5, '0')
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('获取工号失败:', error)
|
||||
// 使用模拟数据
|
||||
previewEmployeeId.value = String(Math.floor(Math.random() * 90000 + 10000)).padStart(5, '0')
|
||||
}
|
||||
}
|
||||
|
||||
// 下一步
|
||||
const nextStep = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate()
|
||||
activeStep.value = 2
|
||||
}
|
||||
|
||||
// 上一步
|
||||
const prevStep = () => {
|
||||
activeStep.value = 1
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
submitting.value = true
|
||||
try {
|
||||
// 动态构建请求参数,fid 来自 login 响应的 id,about_id 来自 login 响应的 about_id
|
||||
const currentInfo = userStore.getAdminInfo()
|
||||
const params = {
|
||||
name: form.name,
|
||||
password: form.password,
|
||||
phone: form.phone,
|
||||
}
|
||||
if (currentInfo?.id) {
|
||||
params.fid = currentInfo.id
|
||||
}
|
||||
if (currentInfo?.about_id) {
|
||||
params.about_id = currentInfo.about_id
|
||||
}
|
||||
const res = await request.post('/admin/employee/add', params)
|
||||
|
||||
if (res.code === 200) {
|
||||
resultData.value = res.data
|
||||
activeStep.value = 3
|
||||
ElMessage.success('添加成功')
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('添加代理失败:', error)
|
||||
ElMessage.error('添加失败,请重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
form.name = ''
|
||||
form.password = ''
|
||||
form.confirmPassword = ''
|
||||
form.phone = ''
|
||||
activeStep.value = 1
|
||||
resultData.value = null
|
||||
fetchNextEmployeeId()
|
||||
}
|
||||
// 一键复制结果信息
|
||||
const copyResultInfo = async () => {
|
||||
if (!resultData.value) return
|
||||
|
||||
const text = `书海寻源代理系统
|
||||
登录网址:https://wallet.buzhiyushu.cn/
|
||||
工号:${resultData.value.employee_id}
|
||||
账号:${resultData.value.username}
|
||||
初始密码:${form.password}
|
||||
手机号:${form.phone}
|
||||
|
||||
请妥善保管账号信息!`
|
||||
|
||||
await copyToClipboard(text, '账号信息已复制到剪贴板')
|
||||
}
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchNextEmployeeId()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.employee-add {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.steps {
|
||||
margin: 20px 0 40px;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
min-height: 300px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.add-form {
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
width: 500px;
|
||||
margin: 30px auto 0;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-item .label {
|
||||
width: 80px;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.preview-item .value {
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.confirm-info {
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.result-info {
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.title-center {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
494
backups/EmployeeList.vue.bak
Normal file
494
backups/EmployeeList.vue.bak
Normal file
@ -0,0 +1,494 @@
|
||||
<template>
|
||||
<div class="employee-list">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>代理列表</span>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="$router.push('/admin/employees/add')">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>添加代理
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索条件 -->
|
||||
<div class="search-form">
|
||||
<el-form :inline="true" :model="queryParams">
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width: 150px">
|
||||
<el-option label="正常" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="搜索">
|
||||
<el-input v-model="queryParams.keyword" placeholder="工号/姓名/账号" clearable style="width: 200px"
|
||||
:prefix-icon="Search" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<el-row :gutter="20" class="stat-cards">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总代理数</div>
|
||||
<div class="stat-value">{{ total }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">正常代理</div>
|
||||
<div class="stat-value success">{{ activeCount }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总积分</div>
|
||||
<div class="stat-value warning">{{ totalPoints }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">平均积分</div>
|
||||
<div class="stat-value info">{{ averagePoints }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 代理列表 -->
|
||||
<el-table :data="employeeList" v-loading="loading" border style="width: 100%">
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="employee_id" label="工号" width="100" align="center" />
|
||||
<el-table-column prop="username" label="账号" width="150" align="center" />
|
||||
<el-table-column prop="name" label="姓名" width="120" align="center" />
|
||||
<el-table-column prop="phone" label="手机号" width="120" align="center" />
|
||||
<el-table-column prop="score" label="积分" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'points-warning': row.score < 100 }">{{ row.score }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="code" label="机械码" width="240" align="center" />
|
||||
<el-table-column prop="level_info" label="等级" width="60" align="center" />
|
||||
<el-table-column prop="last_login_at" label="最后登录" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.last_login_at ? formatDate(row.last_login_at) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="160" align="center" >
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="操作" width="320" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="showTopUp(row)">
|
||||
<el-icon><Coin /></el-icon>充值
|
||||
</el-button>
|
||||
<el-button type="success" size="small" @click="viewAccess(row)">
|
||||
<el-icon><DataLine /></el-icon>积分记录
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="toggleStatus(row, 'disable')"
|
||||
>
|
||||
<el-icon><Switch /></el-icon>禁用
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="success"
|
||||
size="small"
|
||||
@click="toggleStatus(row, 'enable')"
|
||||
>
|
||||
<el-icon><Check /></el-icon>启用
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="showDeduct(row)">
|
||||
<el-icon><Coin /></el-icon>扣减
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination v-model:current-page="queryParams.page" v-model:page-size="queryParams.page_size" :total="total"
|
||||
:page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 充值弹窗 -->
|
||||
<el-dialog v-model="topUpVisible" title="积分充值" width="400px">
|
||||
<el-form :model="topUpForm" ref="topUpFormRef" :rules="topUpRules" label-width="80px">
|
||||
<el-form-item label="代理">
|
||||
<span>{{ currentEmployee?.name }} ({{ currentEmployee?.employee_id }})</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="当前积分">
|
||||
<span>{{ currentEmployee?.points }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值数量" prop="amount">
|
||||
<el-input-number v-model="topUpForm.amount" :min="1" :max="100000" style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="topUpForm.remark" placeholder="选填" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="topUpVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitTopUp" :loading="topUpLoading">确认充值</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 扣减弹窗 -->
|
||||
<el-dialog v-model="deductVisible" title="积分扣减" width="400px">
|
||||
<el-form :model="deductForm" ref="deductFormRef" :rules="deductRules" label-width="80px">
|
||||
<el-form-item label="代理">
|
||||
<span>{{ currentEmployee?.name }} ({{ currentEmployee?.employee_id }})</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="当前积分">
|
||||
<span>{{ currentEmployee?.points }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="扣减数量" prop="amount" class="deduct-label">
|
||||
<el-input-number class="deduct-input" v-model="deductForm.amount" :min="1" :max="currentEmployee?.points || 1"
|
||||
style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="deductForm.remark" placeholder="选填" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="deductVisible = false">取消</el-button>
|
||||
<el-button type="danger" @click="submitDeduct" :loading="deductLoading">确认扣减</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, Coin, DataLine, Switch, Check } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const router = useRouter() // 导入 router
|
||||
const loading = ref(false)
|
||||
const employeeList = ref([])
|
||||
const total = ref(0)
|
||||
const topUpVisible = ref(false)
|
||||
const topUpLoading = ref(false)
|
||||
const deductVisible = ref(false)
|
||||
const deductLoading = ref(false)
|
||||
const currentEmployee = ref(null)
|
||||
|
||||
const queryParams = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
status: '',
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
const topUpForm = reactive({
|
||||
amount: 100,
|
||||
remark: ''
|
||||
})
|
||||
const deductForm = reactive({
|
||||
amount: 100,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const topUpRules = {
|
||||
amount: [
|
||||
{ required: true, message: '请输入充值数量', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '充值数量必须大于0', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const deductRules = {
|
||||
amount: [
|
||||
{ required: true, message: '请输入扣减数量', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '扣减数量必须大于0', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
// 计算统计信息
|
||||
const activeCount = computed(() => {
|
||||
return employeeList.value.filter(e => e.status === 1).length
|
||||
})
|
||||
|
||||
const totalPoints = computed(() => {
|
||||
return employeeList.value.reduce((sum, e) => sum + e.score, 0)
|
||||
})
|
||||
|
||||
const averagePoints = computed(() => {
|
||||
if (employeeList.value.length === 0) return 0
|
||||
return Math.round(totalPoints.value / employeeList.value.length)
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (timestamp) => {
|
||||
if (!timestamp) return '-'
|
||||
const date = new Date(timestamp * 1000)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
// 获取代理列表
|
||||
const fetchEmployeeList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: queryParams.page,
|
||||
page_size: queryParams.page_size
|
||||
}
|
||||
if (queryParams.status !== '') {
|
||||
params.status = queryParams.status
|
||||
}
|
||||
if (queryParams.keyword) {
|
||||
params.keyword = queryParams.keyword
|
||||
}
|
||||
|
||||
const res = await request.get('/admin/employee/list', { params })
|
||||
if (res.code === 200) {
|
||||
employeeList.value = res.data.list
|
||||
total.value = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('获取代理列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
queryParams.page = 1
|
||||
fetchEmployeeList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
queryParams.status = ''
|
||||
queryParams.keyword = ''
|
||||
queryParams.page = 1
|
||||
fetchEmployeeList()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.page_size = size
|
||||
fetchEmployeeList()
|
||||
}
|
||||
|
||||
// 页码变化
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchEmployeeList()
|
||||
}
|
||||
|
||||
// 显示充值弹窗
|
||||
const showTopUp = (row) => {
|
||||
currentEmployee.value = row
|
||||
topUpForm.amount = 100
|
||||
topUpForm.remark = ''
|
||||
topUpVisible.value = true
|
||||
}
|
||||
|
||||
// 提交充值
|
||||
const submitTopUp = async () => {
|
||||
topUpLoading.value = true
|
||||
try {
|
||||
const res = await request.post(`/admin/employee/topup/${currentEmployee.value.employee_id}`, {
|
||||
amount: topUpForm.amount,
|
||||
remark: topUpForm.remark
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('充值成功')
|
||||
topUpVisible.value = false
|
||||
fetchEmployeeList() // 刷新列表
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('充值失败:', error)
|
||||
} finally {
|
||||
topUpLoading.value = false
|
||||
}
|
||||
}
|
||||
// 显示扣减弹窗
|
||||
const showDeduct = (row) => {
|
||||
currentEmployee.value = row
|
||||
deductForm.amount = 100
|
||||
deductForm.remark = ''
|
||||
deductVisible.value = true
|
||||
}
|
||||
|
||||
// 提交扣减
|
||||
const submitDeduct = async () => {
|
||||
deductLoading.value = true
|
||||
try {
|
||||
const res = await request.post(`/admin/employee/deduct/${currentEmployee.value.employee_id}`, {
|
||||
amount: deductForm.amount,
|
||||
remark: deductForm.remark
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('扣减成功')
|
||||
deductVisible.value = false
|
||||
fetchEmployeeList() // 刷新列表
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('扣减失败:', error)
|
||||
} finally {
|
||||
deductLoading.value = false
|
||||
}
|
||||
}
|
||||
// 切换代理状态
|
||||
const toggleStatus = async (row, action) => {
|
||||
const actionText = action === 'enable' ? '启用' : '禁用'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要${actionText}代理 ${row.name} 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await request.post(`/admin/employee/${action}/${row.employee_id}`)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(`${actionText}成功`)
|
||||
fetchEmployeeList() // 刷新列表
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
// console.error('操作失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查看积分记录
|
||||
const viewAccess = (row) => {
|
||||
// 跳转到积分记录页面,并传递代理ID参数
|
||||
router.push({
|
||||
path: '/admin/access',
|
||||
query: {
|
||||
id: row.id,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchEmployeeList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.employee-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
/* 可根据需要保留原有的字体、颜色等样式 */
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-cards {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-value.success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.stat-value.warning {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.stat-value.info {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.points-warning {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.deduct-input :deep(.el-input-number__decrease):hover,
|
||||
.deduct-input :deep(.el-input-number__increase):hover {
|
||||
color: #fff;
|
||||
background-color: #f56c6c;
|
||||
}
|
||||
|
||||
.deduct-label :deep(.el-form-item__label) {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
213
backups/Login.vue.bak
Normal file
213
backups/Login.vue.bak
Normal file
@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<h2>进销存系统</h2>
|
||||
<p>请选择登录角色</p>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="login-tabs">
|
||||
|
||||
<el-tab-pane label="管理员登录" name="admin">
|
||||
<el-form ref="adminFormRef" :model="adminForm" :rules="rules" label-width="0" class="login-form">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="adminForm.username" placeholder="请输入账号" :prefix-icon="User"
|
||||
size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="adminForm.password" type="password" placeholder="请输入密码"
|
||||
:prefix-icon="Lock" size="large" show-password @keyup.enter="handleLogin" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" class="login-btn" size="large"
|
||||
@click="handleLogin">
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="代理登录" name="employee">
|
||||
<el-form ref="employeeFormRef" :model="employeeForm" :rules="rules" label-width="0"
|
||||
class="login-form">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="employeeForm.username" placeholder="请输入账号" :prefix-icon="User"
|
||||
size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="employeeForm.password" type="password" placeholder="请输入密码"
|
||||
:prefix-icon="Lock" size="large" show-password @keyup.enter="handleLogin" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" class="login-btn" size="large"
|
||||
@click="handleLogin">
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
</el-tabs>
|
||||
|
||||
<!-- <div class="login-footer">
|
||||
<p>默认账号:代理 init_00001 / 123456</p>
|
||||
<p>管理员 init_00000 / admin123</p>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
reactive
|
||||
} from 'vue'
|
||||
import {
|
||||
useRouter
|
||||
} from 'vue-router'
|
||||
import {
|
||||
ElMessage
|
||||
} from 'element-plus'
|
||||
import {
|
||||
User,
|
||||
Lock
|
||||
} from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import {
|
||||
setAdminToken,
|
||||
setAdminUserInfo
|
||||
} from '@/utils/auth'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const router = useRouter()
|
||||
const activeTab = ref('admin')
|
||||
const loading = ref(false)
|
||||
|
||||
// 添加 ref 引用
|
||||
const employeeFormRef = ref(null)
|
||||
const adminFormRef = ref(null)
|
||||
|
||||
const employeeForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
about_id:0
|
||||
// username: 'init_00001',
|
||||
// password: '123456'
|
||||
})
|
||||
|
||||
const adminForm = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
about_id:0
|
||||
// username: 'init_00000',
|
||||
// password: 'admin123'
|
||||
})
|
||||
|
||||
const rules = {
|
||||
username: [{
|
||||
required: true,
|
||||
message: '请输入账号',
|
||||
trigger: 'blur'
|
||||
}],
|
||||
password: [{
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: '密码长度不能小于6位',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
const formRef = activeTab.value === 'employee' ? employeeFormRef : adminFormRef
|
||||
const formData = activeTab.value === 'employee' ? employeeForm : adminForm
|
||||
|
||||
await formRef.value.validate()
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const type = activeTab.value === 'admin' ? '255' : '128'
|
||||
const res = await request.post(`/login/${type}`, { ...formData, type: 1 })
|
||||
|
||||
if (res.code === 200) {
|
||||
setAdminToken(res.data.token)
|
||||
setAdminUserInfo(res.data)
|
||||
userStore.setUserInfoAction(res.data)
|
||||
// 每次登录刷新配置为默认值
|
||||
localStorage.setItem('test_ip', '127.0.0.1')
|
||||
localStorage.setItem('test_port', '8080')
|
||||
ElMessage.success('登录成功')
|
||||
// 所有用户登录后都跳转到仪表盘
|
||||
router.push('/dashboard')
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('登录失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
width: 400px;
|
||||
padding: 40px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
</style>
|
||||
475
backups/ReviewIllegalBook.vue.bak
Normal file
475
backups/ReviewIllegalBook.vue.bak
Normal file
@ -0,0 +1,475 @@
|
||||
<template>
|
||||
<div class="review-page">
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>异常书目审核</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索筛选栏 -->
|
||||
<div class="search-bar">
|
||||
<div class="search-item">
|
||||
<span>ISBN:</span>
|
||||
<el-input v-model="searchForm.isbn" placeholder="请输入ISBN" style="width: 180px" clearable />
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<span>状态:</span>
|
||||
<el-select v-model="searchForm.status" placeholder="请选择状态" style="width: 140px" clearable>
|
||||
<el-option label="待审核" value="0" />
|
||||
<el-option label="已通过" value="1" />
|
||||
<el-option label="已驳回" value="2" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column prop="status" label="状态" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" size="small">
|
||||
{{ statusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="barcode" label="ISBN" width="160" align="center" />
|
||||
<el-table-column label="书名" min-width="200" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
|
||||
<template #reference>
|
||||
<span :class="{ 'diff-cell': isChanged(row.old_name, row.new_name) }">{{ row.old_name }}</span>
|
||||
</template>
|
||||
<div class="diff-content">
|
||||
<div class="diff-row old"><span class="diff-label">旧值:</span><span class="diff-val">{{ row.old_name }}</span></div>
|
||||
<div class="diff-row new"><span class="diff-label">新值:</span><span class="diff-val">{{ row.new_name }}</span></div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="作者" width="150" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
|
||||
<template #reference>
|
||||
<span :class="{ 'diff-cell': isChanged(row.old_author, row.new_author) }">{{ row.old_author }}</span>
|
||||
</template>
|
||||
<div class="diff-content">
|
||||
<div class="diff-row old"><span class="diff-label">旧值:</span><span class="diff-val">{{ row.old_author }}</span></div>
|
||||
<div class="diff-row new"><span class="diff-label">新值:</span><span class="diff-val">{{ row.new_author }}</span></div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="出版社" width="150" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
|
||||
<template #reference>
|
||||
<span :class="{ 'diff-cell': isChanged(row.old_publisher, row.new_publisher) }">{{ row.old_publisher }}</span>
|
||||
</template>
|
||||
<div class="diff-content">
|
||||
<div class="diff-row old"><span class="diff-label">旧值:</span><span class="diff-val">{{ row.old_publisher }}</span></div>
|
||||
<div class="diff-row new"><span class="diff-label">新值:</span><span class="diff-val">{{ row.new_publisher }}</span></div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="套装书" width="150" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
|
||||
<template #reference>
|
||||
<el-tag type="info" size="small" :class="{ 'diff-cell': isChanged(row.old_is_suit, row.new_is_suit) }">
|
||||
{{ row.old_is_suit === 0 ? '非套装书' : '套装书' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<div class="diff-content">
|
||||
<div class="diff-row old"><span class="diff-label">旧值:</span><span class="diff-val">{{ row.old_is_suit === 0 ? '非套装书' : '套装书' }}</span></div>
|
||||
<div class="diff-row new"><span class="diff-label">新值:</span><span class="diff-val">{{ row.new_is_suit === 0 ? '非套装书' : '套装书' }}</span></div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="定价" width="150" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
|
||||
<template #reference>
|
||||
<span :class="{ 'diff-cell': isChanged(row.old_price, row.new_price) }">{{ row.old_price }}</span>
|
||||
</template>
|
||||
<div class="diff-content">
|
||||
<div class="diff-row old"><span class="diff-label">旧值:</span><span class="diff-val">{{ row.old_price }}</span></div>
|
||||
<div class="diff-row new"><span class="diff-label">新值:</span><span class="diff-val">{{ row.new_price }}</span></div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="装帧" width="150" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
|
||||
<template #reference>
|
||||
<span :class="{ 'diff-cell': isChanged(row.old_binding_layout, row.new_binding_layout) }">{{ row.old_binding_layout }}</span>
|
||||
</template>
|
||||
<div class="diff-content">
|
||||
<div class="diff-row old"><span class="diff-label">旧值:</span><span class="diff-val">{{ row.old_binding_layout }}</span></div>
|
||||
<div class="diff-row new"><span class="diff-label">新值:</span><span class="diff-val">{{ row.new_binding_layout }}</span></div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="页数" width="150" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
|
||||
<template #reference>
|
||||
<span :class="{ 'diff-cell': isChanged(row.old_page_count, row.new_page_count) }">{{ row.old_page_count }}</span>
|
||||
</template>
|
||||
<div class="diff-content">
|
||||
<div class="diff-row old"><span class="diff-label">旧值:</span><span class="diff-val">{{ row.old_page_count }}</span></div>
|
||||
<div class="diff-row new"><span class="diff-label">新值:</span><span class="diff-val">{{ row.new_page_count }}</span></div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字数" width="150" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
|
||||
<template #reference>
|
||||
<span :class="{ 'diff-cell': isChanged(row.old_word_count, row.new_word_count) }">{{ row.old_word_count }}</span>
|
||||
</template>
|
||||
<div class="diff-content">
|
||||
<div class="diff-row old"><span class="diff-label">旧值:</span><span class="diff-val">{{ row.old_word_count }}</span></div>
|
||||
<div class="diff-row new"><span class="diff-label">新值:</span><span class="diff-val">{{ row.new_word_count }}</span></div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="出版时间" width="150" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<el-popover placement="bottom" :width="280" trigger="hover" popper-class="diff-popover">
|
||||
<template #reference>
|
||||
<span :class="{ 'diff-cell': isPubTimeChanged(row.old_publication_time, row.new_publication_time) }">{{ formatPubTime(row.old_publication_time) }}</span>
|
||||
</template>
|
||||
<div class="diff-content">
|
||||
<div class="diff-row old"><span class="diff-label">旧值:</span><span class="diff-val">{{ formatPubTime(row.old_publication_time) }}</span></div>
|
||||
<div class="diff-row new"><span class="diff-label">新值:</span><span class="diff-val">{{ formatPubTime(row.new_publication_time) }}</span></div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提交时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" @click="handleApprove(row)">
|
||||
查看
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleReject(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handlePageChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 查看详情弹窗 -->
|
||||
<el-dialog v-model="detailVisible" title="调剂记录详情" width="60%" :close-on-click-modal="false">
|
||||
<template v-if="detailData">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="ISBN">{{ detailData.isbn }}</el-descriptions-item>
|
||||
<el-descriptions-item label="书名">{{ detailData.bookName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="作者">{{ detailData.author }}</el-descriptions-item>
|
||||
<el-descriptions-item label="出版社">{{ detailData.publisher }}</el-descriptions-item>
|
||||
<el-descriptions-item label="异常类型">{{ detailData.abnormalType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusTagType(detailData.status)" size="small">
|
||||
{{ statusText(detailData.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常说明" :span="2">
|
||||
{{ detailData.abnormalDesc || '无' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="提交时间">{{ detailData.createdAt }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detailData.reviewTime" label="审核时间">{{ detailData.reviewTime
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 审核弹窗 - 使用 SubmIllegalBook 组件 -->
|
||||
<SubmIllegalBook
|
||||
v-model:visible="reviewDialogVisible"
|
||||
:review-data="reviewRecord"
|
||||
:goods="null"
|
||||
@review="handleReviewResult"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import SubmIllegalBook from '@/components/dialog/submIllegalBook/SubmIllegalBook.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
isbn: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 详情弹窗
|
||||
const detailVisible = ref(false)
|
||||
const detailData = ref<any>(null)
|
||||
|
||||
// 审核弹窗
|
||||
const reviewDialogVisible = ref(false)
|
||||
const reviewRecord = ref<any>(null)
|
||||
|
||||
// 状态标签样式
|
||||
function statusTagType(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
0: 'warning',
|
||||
1: 'success',
|
||||
2: 'danger'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
// 格式化出版时间为 yyyy-mm(输入为10位时间戳,秒级)
|
||||
function formatPubTime(time: any): string {
|
||||
if (time === null || time === undefined || time === '') return ''
|
||||
const ts = Number(time)
|
||||
if (isNaN(ts) || ts <= 0) return ''
|
||||
const d = new Date(ts * 1000)
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
return `${y}-${m}`
|
||||
}
|
||||
|
||||
// 格式化为完整时间 yyyy-mm-dd HH:mm:ss(输入为10位时间戳,秒级)
|
||||
function formatDateTime(time: any): string {
|
||||
if (time === null || time === undefined || time === '') return ''
|
||||
const ts = Number(time)
|
||||
if (isNaN(ts) || ts <= 0) return ''
|
||||
const d = new Date(ts * 1000)
|
||||
const y = d.getFullYear()
|
||||
const M = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const h = String(d.getHours()).padStart(2, '0')
|
||||
const m = String(d.getMinutes()).padStart(2, '0')
|
||||
const s = String(d.getSeconds()).padStart(2, '0')
|
||||
return `${y}-${M}-${day} ${h}:${m}:${s}`
|
||||
}
|
||||
|
||||
// 判断新旧数据是否不同
|
||||
function isChanged(oldVal: any, newVal: any): boolean {
|
||||
// 如果新旧值都是空值,视为相同
|
||||
if ((oldVal === null || oldVal === undefined || oldVal === '') &&
|
||||
(newVal === null || newVal === undefined || newVal === '')) {
|
||||
return false
|
||||
}
|
||||
return String(oldVal) !== String(newVal)
|
||||
}
|
||||
|
||||
// 判断出版时间是否不同(处理时间戳空值情况)
|
||||
function isPubTimeChanged(oldTs: any, newTs: any): boolean {
|
||||
return isChanged(formatPubTime(oldTs), formatPubTime(newTs))
|
||||
}
|
||||
|
||||
// 状态文本
|
||||
function statusText(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
0: '待审核',
|
||||
1: '已通过',
|
||||
2: '已驳回'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
// 查询
|
||||
function handleSearch() {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchForm.isbn = ''
|
||||
searchForm.status = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// 多选
|
||||
function handleSelectionChange(rows: any[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
function handlePageChange() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await request.get('/product_log/list', {
|
||||
params: {
|
||||
barcode: searchForm.isbn || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize
|
||||
}
|
||||
})
|
||||
const data = res?.data
|
||||
if (data) {
|
||||
tableData.value = Array.isArray(data.list) ? data.list : Array.isArray(data) ? data : []
|
||||
pagination.total = data.total || 0
|
||||
} else {
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} catch (err) {
|
||||
ElMessage.error('获取列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 审核 - 打开审核弹窗
|
||||
function handleApprove(row: any) {
|
||||
reviewRecord.value = row
|
||||
reviewDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
async function handleReject(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该条记录?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
const formData = new FormData()
|
||||
formData.append('id', String(row.id))
|
||||
await request.post('/product_log/delete', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
ElMessage.success('已删除')
|
||||
fetchData()
|
||||
} catch {
|
||||
// 取消
|
||||
}
|
||||
}
|
||||
|
||||
// 审核结果处理 - 弹窗关闭后刷新列表
|
||||
function handleReviewResult(action: 'approve' | 'reject', record: any) {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function handleViewDetail(row: any) {
|
||||
detailData.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.review-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-item span {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 新旧数据对比 - 单元格变红 */
|
||||
.diff-cell {
|
||||
color: #f56c6c !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Popover 内容样式(全局,因 popover 渲染在 body 下) */
|
||||
.diff-popover {
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
.diff-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.diff-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.diff-label {
|
||||
white-space: nowrap;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.diff-val {
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
.diff-row.new .diff-val {
|
||||
color: #e6a23c;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
771
backups/ShippingOrder.vue.bak
Normal file
771
backups/ShippingOrder.vue.bak
Normal file
@ -0,0 +1,771 @@
|
||||
<template>
|
||||
<el-card class="shipping-order-manager">
|
||||
<template #header>
|
||||
<div class="card-header">发货单管理</div>
|
||||
</template>
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="searchParams.keyword" placeholder="发货单号" clearable style="width: 200px"
|
||||
@keyup.enter="handleSearch">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select v-model="searchParams.status" placeholder="状态" clearable style="width: 120px">
|
||||
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<el-select v-model="searchParams.warehouse_id" placeholder="仓库" clearable style="width: 140px">
|
||||
<el-option v-for="item in warehouseOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<el-select v-model="searchParams.customer_id" placeholder="客户" clearable filterable style="width: 160px">
|
||||
<el-option v-for="item in customerOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%"
|
||||
@expand-change="handleExpandChange">
|
||||
|
||||
<!-- 发货单详情展开行 -->
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div v-if="detailCache[row.id]" style="padding: 12px 20px">
|
||||
<h4 style="margin: 0 0 10px; font-size: 14px; color: #303133">发货明细</h4>
|
||||
<el-table :data="detailCache[row.id].items || []" border size="small">
|
||||
<el-table-column prop="product_name" label="商品名称" min-width="120" show-overflow-tooltip align="center" />
|
||||
<el-table-column prop="product_code" label="ISBN/条码" min-width="100" show-overflow-tooltip
|
||||
align="center" />
|
||||
<el-table-column label="库位" min-width="100" align="center">
|
||||
<template #default="{ row: item }">
|
||||
{{ locationMap[item.warehouse_code] || item.warehouse_code || '-' }}##{{
|
||||
locationMap[item.location_name]
|
||||
|| item.location_name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销售单号" min-width="140" align="center">
|
||||
<template #default="{ row: item }">
|
||||
<a v-if="item.sales_order_no" style="color: #409eff; cursor: pointer; text-decoration: underline;"
|
||||
@click.stop="navigateToSalesOrder(item.sales_order_no)">
|
||||
{{ item.sales_order_no }}
|
||||
</a>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="出库单号" min-width="140" align="center">
|
||||
<template #default="{ row: item }">
|
||||
<a v-if="item.outbound_order_no" style="color: #409eff; cursor: pointer; text-decoration: underline;"
|
||||
@click.stop="navigateToOutbound(item.outbound_order_no)">
|
||||
{{ item.outbound_order_no }}
|
||||
</a>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="association_order_no" label="平台单号" min-width="90" align="center">
|
||||
<template #default="{ row: item }">
|
||||
<span style="display: inline-flex; align-items: center; gap: 4px;">
|
||||
<span v-if="item.association_order_no" style="color: #409eff; text-decoration: underline;">{{ item.association_order_no }}</span>
|
||||
<span v-else>-</span>
|
||||
<el-button v-if="item.association_order_no" type="primary" size="small" link
|
||||
@click="copyToClipboard(item.association_order_no, '平台单号已复制')">
|
||||
<el-icon>
|
||||
<CopyDocument />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销售时间" min-width="100" align="center">
|
||||
<template #default="{ row: item }">{{ formatDateForSale(item.sales_order_created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="售价" min-width="40" align="center">
|
||||
<template #default="{ row: item }">
|
||||
<span style="color: #e6a23c; font-weight: 600">{{ formatAmount(item.unit_price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quantity" label="出库数量" min-width="60" align="center">
|
||||
<template #default="{ row: item }">
|
||||
<span style="color: #409eff; font-weight: 600">{{ item.quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="快递公司" min-width="100" align="center">
|
||||
<template #default="{ row: item }">{{ logisticsCompanyMap[item.logistics_company] ||
|
||||
item.logistics_company || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="logistics_no" label="快递单号" min-width="100" align="center">
|
||||
<template #default="{ row: item }">{{ item.logistics_no || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="receiver_address" label="收货地址" min-width="100" align="center">
|
||||
<template #default="{ row: item }">{{ item.receiver_address || '-' }}</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else style="padding: 20px; text-align: center; color: #909399">
|
||||
<el-icon class="is-loading" style="margin-right: 6px">
|
||||
<Loading />
|
||||
</el-icon>加载中...
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="shipping_no" label="发货单号" min-width="160" show-overflow-tooltip align="center" />
|
||||
<el-table-column label="客户" min-width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.shop_list && row.shop_list.length > 0">
|
||||
<div v-for="(shop, idx) in row.shop_list" :key="idx">
|
||||
{{ shop.shop_name }}({{ shop.shop_type_text }})
|
||||
</div>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status_text" label="状态" min-width="80" show-overflow-tooltip align="center" />
|
||||
<el-table-column prop="operator" label="操作员" min-width="90" align="center">
|
||||
<template #default="{ row }">{{ row.operator || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="150" align="center">
|
||||
<template #default="{ row }">{{ formatTimestamp(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" link @click="handleStartShipping(row)">开始发货</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="" align="center" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" link>打单</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||||
</div>
|
||||
|
||||
<!-- 扫码发货弹窗 -->
|
||||
<el-dialog v-model="scanDialogVisible" title="扫码发货" width="1000px" :close-on-click-modal="false"
|
||||
@opened="onScanDialogOpened" @closed="onScanDialogClosed">
|
||||
<div style="padding: 10px">
|
||||
<p style="margin-bottom: 12px; color: #606266; text-align: center">
|
||||
共 <strong style="color:#409eff">{{ currentScanTotal }}</strong> 件商品,已扫描 <strong style="color:#67c23a">{{
|
||||
currentScanIndex }}</strong> 件
|
||||
</p>
|
||||
|
||||
<el-table :data="scanList" border size="small" style="width: 100%" max-height="340"
|
||||
:row-class-name="scanRowClassName">
|
||||
<el-table-column type="index" label="#" width="40" align="center" />
|
||||
<el-table-column prop="product_name" label="商品名称" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="product_code" label="ISBN" width="120" align="center" />
|
||||
<el-table-column prop="quantity" label="数量" width="60" align="center" />
|
||||
<el-table-column prop="association_order_no" label="平台单号" min-width="150" align="center"
|
||||
show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.association_order_no" style="display: inline-flex; align-items: center; gap: 4px;">
|
||||
<span style="color: #409eff; text-decoration: underline;">{{ row.association_order_no }}</span>
|
||||
<el-button type="primary" size="small" link
|
||||
@click="copyToClipboard(row.association_order_no, '平台单号已复制')">
|
||||
<el-icon>
|
||||
<CopyDocument />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="快递单号" min-width="120" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<span v-if="$index < currentScanIndex && row.logistics_no" style="color: #67c23a;">{{ row.logistics_no
|
||||
}}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="售价" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #e6a23c; font-weight: 600">{{ formatAmount(row.unit_price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row, $index }">
|
||||
<template v-if="$index < currentScanIndex || row.logistics_no">
|
||||
<el-tag type="success" size="small">已扫</el-tag>
|
||||
</template>
|
||||
<template v-else-if="$index === processingIndex">
|
||||
<el-tag type="warning" size="small" effect="dark">扫描中</el-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag type="info" size="small">待扫</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-input ref="scanInputRef" v-model="scanCode" placeholder="请用扫码枪扫描 ISBN..." @keyup.enter="handleScanSubmit"
|
||||
class="hidden-scan-input" />
|
||||
<p style="margin-top: 8px; color: #c0c4cc; font-size: 12px; text-align: center">请使用扫码枪扫描商品条码</p>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, reactive, onMounted, computed, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh, View, Loading, CopyDocument } from '@element-plus/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import axios from 'axios'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import { fetchShippingOrderList, fetchShippingOrderDetail, updateShippingOrderLogistics } from '@/api/shipping-order'
|
||||
import { fetchWarehouseList } from '@/api/warehouse'
|
||||
import { createPrintTask } from '@/api/print'
|
||||
|
||||
/** 状态映射 */
|
||||
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
||||
1: { label: '已创建', type: 'info' },
|
||||
2: { label: '拣货中', type: 'warning' },
|
||||
3: { label: '已完成', type: 'success' },
|
||||
4: { label: '已取消', type: 'danger' }
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ShippingOrder',
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const loading = ref<boolean>(false)
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
/** 导航到销售订单页面 */
|
||||
const navigateToSalesOrder = (salesOrderNo: string) => {
|
||||
router.push({ name: 'sales-order', query: { keyword: salesOrderNo } })
|
||||
}
|
||||
|
||||
/** 导航到出库单页面 */
|
||||
const navigateToOutbound = (outboundOrderNo: string) => {
|
||||
router.push({ name: 'outbound', query: { keyword: outboundOrderNo } })
|
||||
}
|
||||
|
||||
const statusOptions = Object.entries(STATUS_MAP).map(([value, { label }]) => ({
|
||||
value: Number(value),
|
||||
label
|
||||
}))
|
||||
|
||||
const searchParams = reactive<{
|
||||
keyword: string
|
||||
status: number | null
|
||||
warehouse_id: number | null
|
||||
customer_id: number | null
|
||||
}>({
|
||||
keyword: '',
|
||||
status: null,
|
||||
warehouse_id: null,
|
||||
customer_id: null
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const detailVisible = ref<boolean>(false)
|
||||
const detailData = ref<any>(null)
|
||||
|
||||
// 下拉选项
|
||||
const warehouseOptions = ref<any[]>([])
|
||||
const customerOptions = ref<any[]>([])
|
||||
|
||||
// ID→名称映射
|
||||
const warehouseMap = ref<Record<string, string>>({})
|
||||
const customerMap = ref<Record<string, string>>({})
|
||||
const salesOrderMap = ref<Record<string, string>>({})
|
||||
const waveTaskMap = ref<Record<string, string>>({})
|
||||
const locationMap = ref<Record<string, string>>({})
|
||||
|
||||
// 详情缓存
|
||||
const detailCache = ref<Record<number, any>>({})
|
||||
|
||||
// ========== 扫码发货相关 ==========
|
||||
const scanDialogVisible = ref(false)
|
||||
const scanCode = ref('')
|
||||
const scanInputRef = ref<any>(null)
|
||||
const scanList = ref<any[]>([]) // 当前发货单的明细列表
|
||||
const currentScanIndex = ref(0) // 当前扫描到的索引
|
||||
const currentScanTotal = ref(0) // 总数量
|
||||
const scanFocusTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const currentScanItem = computed(() => scanList.value[currentScanIndex.value] || null)
|
||||
|
||||
const formatAmount = (amount: number): string => {
|
||||
if (!amount && amount !== 0) return '¥0.00'
|
||||
return '¥' + (Number(amount) / 100).toFixed(2)
|
||||
}
|
||||
|
||||
// 扫描防并发锁
|
||||
const isProcessing = ref(false)
|
||||
const processingIndex = ref(-1)
|
||||
|
||||
/** 已扫描完成的行添加背景色 */
|
||||
const scanRowClassName = ({ row, rowIndex }: { row: any; rowIndex: number }) => {
|
||||
if (rowIndex < currentScanIndex.value || row.logistics_no) {
|
||||
return 'scan-completed-row'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 点击 "开始发货" */
|
||||
const handleStartShipping = async (row: any) => {
|
||||
// 优先用缓存,否则重新请求
|
||||
let detail = detailCache.value[row.id]
|
||||
if (!detail || !detail.items) {
|
||||
try {
|
||||
detail = await fetchShippingOrderDetail(row.id)
|
||||
detailCache.value[row.id] = detail
|
||||
} catch (e) {
|
||||
ElMessage.error('加载发货明细失败')
|
||||
return
|
||||
}
|
||||
}
|
||||
const items = detail?.items || []
|
||||
if (items.length === 0) {
|
||||
ElMessage.warning('该发货单没有明细')
|
||||
return
|
||||
}
|
||||
scanList.value = items.map((item: any) => ({ ...item, shipping_order_id: row.id }))
|
||||
// 根据实际数据设置初始状态:已有物流单号的视为已扫描
|
||||
const firstUnscanned = scanList.value.findIndex((item: any) => !item.logistics_no)
|
||||
currentScanIndex.value = firstUnscanned >= 0 ? firstUnscanned : scanList.value.length
|
||||
currentScanTotal.value = items.length
|
||||
scanCode.value = ''
|
||||
scanDialogVisible.value = true
|
||||
}
|
||||
|
||||
/** 弹窗打开后聚焦并锁定焦点到隐藏 input */
|
||||
const onScanDialogOpened = () => {
|
||||
const focusInput = () => {
|
||||
scanInputRef.value?.focus?.()
|
||||
scanInputRef.value?.$el?.querySelector?.('input')?.focus?.()
|
||||
}
|
||||
// 立刻聚焦一次
|
||||
nextTick(() => { focusInput() })
|
||||
// 每隔 200ms 强制保持焦点(直到弹窗关闭)
|
||||
const intervalId = setInterval(() => {
|
||||
if (!scanDialogVisible.value) {
|
||||
clearInterval(intervalId)
|
||||
return
|
||||
}
|
||||
// 检查当前焦点是否已不在该输入框上,是则重新聚焦
|
||||
const active = document.activeElement
|
||||
const inputEl = scanInputRef.value?.$el?.querySelector?.('input')
|
||||
if (inputEl && active !== inputEl) {
|
||||
inputEl.focus()
|
||||
}
|
||||
}, 200)
|
||||
// dialog 关闭时清理 interval
|
||||
const origClose = onScanDialogClosed
|
||||
const cleanup = () => {
|
||||
clearInterval(intervalId)
|
||||
origClose()
|
||||
}
|
||||
}
|
||||
|
||||
/** 弹窗关闭时清理 */
|
||||
const onScanDialogClosed = () => {
|
||||
scanCode.value = ''
|
||||
scanList.value = []
|
||||
currentScanIndex.value = 0
|
||||
currentScanTotal.value = 0
|
||||
}
|
||||
|
||||
/** 扫码回车处理 */
|
||||
const handleScanSubmit = async () => {
|
||||
const code = scanCode.value.trim()
|
||||
if (!code) return
|
||||
|
||||
// 如果正在处理中,忽略新的扫码
|
||||
if (isProcessing.value) return
|
||||
|
||||
// 锁定
|
||||
isProcessing.value = true
|
||||
processingIndex.value = currentScanIndex.value
|
||||
|
||||
// 在整个发货单明细中查找 ISBN 匹配的商品
|
||||
const matchIndex = scanList.value.findIndex(
|
||||
(item: any) => item.product_code === code
|
||||
)
|
||||
|
||||
if (matchIndex === -1) {
|
||||
ElMessage.warning(`未找到 ISBN 为 ${code} 的商品`)
|
||||
isProcessing.value = false
|
||||
processingIndex.value = -1
|
||||
scanCode.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 匹配成功:交换当前扫描位与匹配位(把匹配商品提到当前位置)
|
||||
if (matchIndex !== currentScanIndex.value) {
|
||||
const temp = scanList.value[currentScanIndex.value]
|
||||
scanList.value[currentScanIndex.value] = scanList.value[matchIndex]
|
||||
scanList.value[matchIndex] = temp
|
||||
}
|
||||
|
||||
const currentItem = scanList.value[currentScanIndex.value]
|
||||
if (!currentItem) {
|
||||
isProcessing.value = false
|
||||
processingIndex.value = -1
|
||||
return
|
||||
}
|
||||
|
||||
const salesPersonId = currentItem.sales_person_id
|
||||
if (!salesPersonId) {
|
||||
ElMessage.warning('当前商品缺少 sales_person_id')
|
||||
isProcessing.value = false
|
||||
processingIndex.value = -1
|
||||
scanCode.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 第一步:获取快递账号列表
|
||||
const res = await axios.get('https://api.buzhiyushu.cn/zhishu/fastMail/listApi', {
|
||||
params: { shopId: salesPersonId }
|
||||
})
|
||||
console.log('fastMail/listApi 响应:', res.data)
|
||||
|
||||
// 第二步:提取 fastMailType=1 的成员,调用创建面单接口
|
||||
const data = res.data?.data
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
ElMessage.error('未获取到快递账号配置')
|
||||
isProcessing.value = false
|
||||
processingIndex.value = -1
|
||||
scanCode.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const fastMail = data.find((item: any) => item.fastMailType === '1')
|
||||
if (!fastMail || !currentItem.association_order_no) {
|
||||
ElMessage.error('未找到默认快递配置或缺少平台单号')
|
||||
isProcessing.value = false
|
||||
processingIndex.value = -1
|
||||
scanCode.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('type', fastMail.type)
|
||||
formData.append('partnerId', fastMail.partnerId)
|
||||
formData.append('secret', fastMail.secret)
|
||||
formData.append('orderSn', currentItem.association_order_no)
|
||||
formData.append('contact', currentItem.warehouse_contact_person)
|
||||
formData.append('phoneNumber', currentItem.warehouse_contact_phone)
|
||||
formData.append('province', currentItem.warehouse_province)
|
||||
formData.append('city', currentItem.warehouse_city)
|
||||
formData.append('area', currentItem.warehouse_district)
|
||||
formData.append('town', currentItem.warehouse_address)
|
||||
const createRes = await axios.post('https://print.buzhiyushu.cn/api/print/createOrderBatch', formData)
|
||||
console.log('createOrderBatch 响应:', createRes.data)
|
||||
|
||||
// 第三步:获取打印PDF信息
|
||||
const createData = await axios.get('https://print.buzhiyushu.cn/api/print/createBmOrderDaYin', {
|
||||
params: {
|
||||
mailno: createRes.data.data[0].mail_no || createRes.data.data[0].mailno,
|
||||
partnerId: fastMail.partnerId,
|
||||
secret: fastMail.secret
|
||||
}
|
||||
})
|
||||
console.log('createBmOrderDaYin 响应:', createData.data)
|
||||
|
||||
const printData = createRes.data.data[0]
|
||||
printData.pdf_info = createData.data.pdfInfo
|
||||
printData.itemList = createRes.data.erpGoodsOrderList[0].itemList;
|
||||
console.log('createBmOrderDaYin 响应:', JSON.stringify(printData))
|
||||
|
||||
// 调用createPrintTask接口创建打印任务并打印(失败会阻断后续回填操作)
|
||||
console.log('正在创建打印任务并打印...')
|
||||
const LODOP = await createPrintTask('yunda', printData) as any
|
||||
LODOP.SET_PRINTER_INDEX(localStorage.getItem('printer_express'));
|
||||
const printResult = LODOP.PRINT()
|
||||
console.log('打印结果:', printResult)
|
||||
|
||||
// 调用updateShippingOrderLogistics更新物流信息(失败会阻断后续回填操作)
|
||||
if (currentItem.sales_order_item_id) {
|
||||
await updateShippingOrderLogistics({
|
||||
shipping_order_id: currentItem.shipping_order_id,
|
||||
sales_order_item_id: currentItem.sales_order_item_id,
|
||||
logistics_company: fastMail.type,
|
||||
logistics_no: createRes.data.data[0].mail_no || createRes.data.data[0].mailno
|
||||
})
|
||||
}
|
||||
|
||||
console.log('正在回填快递单号并更新状态...')
|
||||
const submitCompany = await axios.post('https://api.buzhiyushu.cn/zhishu/orderExternalGoods/submitCompanyOrder', {
|
||||
code: fastMail.type,
|
||||
orderNo: createRes.data.data[0].mail_no || createRes.data.data[0].mailno,
|
||||
erpOrderId: createRes.data.erpGoodsOrderList[0].id.toString()
|
||||
}, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
console.log('回填快递单号 响应:', submitCompany.data)
|
||||
|
||||
// 回填快递单号到当前行
|
||||
scanList.value[currentScanIndex.value].logistics_no = createRes.data.data[0].mail_no || createRes.data.data[0].mailno
|
||||
} catch (err) {
|
||||
console.error('发货处理失败:', err)
|
||||
ElMessage.error('发货处理失败,请重试')
|
||||
isProcessing.value = false
|
||||
processingIndex.value = -1
|
||||
scanCode.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 解锁,前进到下一个商品
|
||||
isProcessing.value = false
|
||||
processingIndex.value = -1
|
||||
currentScanIndex.value++
|
||||
scanCode.value = ''
|
||||
|
||||
if (currentScanIndex.value >= scanList.value.length) {
|
||||
ElMessage.success('全部扫描完成!')
|
||||
scanDialogVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const statusLabel = (status: number): string => STATUS_MAP[status]?.label || '未知'
|
||||
const statusTagType = (status: number): string => STATUS_MAP[status]?.type || 'info'
|
||||
|
||||
const formatTimestamp = (timestamp?: number | string | null): string => {
|
||||
if (!timestamp && timestamp !== 0) return '-'
|
||||
return dayjs.unix(Number(timestamp)).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const formatDate = (date?: number | string | null): string => {
|
||||
if (!date) return '-'
|
||||
if (typeof date === 'number' && date < 10000000000) {
|
||||
return dayjs.unix(date).format('YYYY-MM-DD')
|
||||
}
|
||||
return dayjs(date).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
|
||||
const formatDateForSale = (date?: number | string | null): string => {
|
||||
if (!date) return '-'
|
||||
if (typeof date === 'number' && date < 10000000000) {
|
||||
return dayjs.unix(date).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
/** 快递公司映射 */
|
||||
const logisticsCompanyMap: Record<string, string> = {
|
||||
YUNDA: '韵达快递'
|
||||
}
|
||||
|
||||
/** 加载下拉选项和名称映射 */
|
||||
const loadOptions = async (): Promise<void> => {
|
||||
try {
|
||||
// 加载仓库
|
||||
const warehouseRes = await fetchWarehouseList({ keyword: '', page: 1, pageSize: 9999 })
|
||||
warehouseOptions.value = warehouseRes.list || []
|
||||
const wMap: Record<string, string> = {}
|
||||
for (const w of warehouseRes.list) {
|
||||
wMap[String(w.id)] = w.name || w.code || String(w.id)
|
||||
}
|
||||
warehouseMap.value = wMap
|
||||
|
||||
// TODO: 加载客户、销售订单、波次任务、库位选项(需要对应API)
|
||||
customerOptions.value = []
|
||||
customerMap.value = {}
|
||||
} catch (error) {
|
||||
// console.error('加载选项失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载发货单列表 */
|
||||
const loadList = async (): Promise<void> => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchShippingOrderList({
|
||||
check_no: searchParams.keyword,
|
||||
status: searchParams.status || undefined,
|
||||
warehouse_id: searchParams.warehouse_id || undefined,
|
||||
customer_id: searchParams.customer_id || undefined,
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total || 0
|
||||
} catch (error) {
|
||||
// console.error('加载发货单列表失败:', error)
|
||||
ElMessage.error('加载发货单列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
const handleSearch = (): void => {
|
||||
pagination.current = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
/** 重置搜索 */
|
||||
const resetSearch = (): void => {
|
||||
searchParams.keyword = ''
|
||||
searchParams.status = null
|
||||
searchParams.warehouse_id = null
|
||||
searchParams.customer_id = null
|
||||
pagination.current = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
/** 分页大小变化 */
|
||||
const handleSizeChange = (size: number): void => {
|
||||
pagination.pageSize = size
|
||||
loadList()
|
||||
}
|
||||
|
||||
/** 页码变化 */
|
||||
const handleCurrentChange = (page: number): void => {
|
||||
pagination.current = page
|
||||
loadList()
|
||||
}
|
||||
|
||||
/** 展开行时加载详情 */
|
||||
const handleExpandChange = async (row: any, expandedRows: any[]): Promise<void> => {
|
||||
if (expandedRows.find((r: any) => r.id === row.id) && !detailCache.value[row.id]) {
|
||||
try {
|
||||
const detail = await fetchShippingOrderDetail(row.id)
|
||||
detailCache.value[row.id] = detail
|
||||
} catch (error) {
|
||||
// console.error('加载发货单详情失败:', error)
|
||||
detailCache.value[row.id] = { items: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 查看发货单详情 */
|
||||
const handleView = async (row: any): Promise<void> => {
|
||||
try {
|
||||
const detail = await fetchShippingOrderDetail(row.id)
|
||||
detailData.value = detail
|
||||
detailVisible.value = true
|
||||
} catch (error) {
|
||||
// console.error('加载发货单详情失败:', error)
|
||||
ElMessage.error('加载详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadOptions()
|
||||
loadList()
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
tableData,
|
||||
statusOptions,
|
||||
searchParams,
|
||||
pagination,
|
||||
detailVisible,
|
||||
detailData,
|
||||
warehouseOptions,
|
||||
customerOptions,
|
||||
warehouseMap,
|
||||
customerMap,
|
||||
salesOrderMap,
|
||||
waveTaskMap,
|
||||
locationMap,
|
||||
detailCache,
|
||||
scanDialogVisible,
|
||||
scanInputRef,
|
||||
scanCode,
|
||||
scanList,
|
||||
currentScanIndex,
|
||||
currentScanTotal,
|
||||
currentScanItem,
|
||||
isProcessing,
|
||||
processingIndex,
|
||||
formatAmount,
|
||||
scanRowClassName,
|
||||
handleStartShipping,
|
||||
onScanDialogOpened,
|
||||
onScanDialogClosed,
|
||||
handleScanSubmit,
|
||||
statusLabel,
|
||||
statusTagType,
|
||||
formatTimestamp,
|
||||
formatDate,
|
||||
formatDateForSale,
|
||||
logisticsCompanyMap,
|
||||
handleSearch,
|
||||
resetSearch,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
handleExpandChange,
|
||||
handleView,
|
||||
navigateToSalesOrder,
|
||||
navigateToOutbound,
|
||||
copyToClipboard,
|
||||
Search,
|
||||
Refresh,
|
||||
View,
|
||||
Loading,
|
||||
CopyDocument
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.shipping-order-manager {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-table__expanded-cell) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 隐藏扫码输入框,仅保留焦点功能 */
|
||||
.hidden-scan-input {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
/* 已完成扫描的行背景色 */
|
||||
:deep(.el-table__row.scan-completed-row) {
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.scan-completed-row:hover > td) {
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
</style>
|
||||
3364
backups/camera.vue.bak
Normal file
3364
backups/camera.vue.bak
Normal file
File diff suppressed because it is too large
Load Diff
97
backups/config.js.bak
Normal file
97
backups/config.js.bak
Normal file
@ -0,0 +1,97 @@
|
||||
import axios from 'axios'
|
||||
|
||||
/**
|
||||
* 测试连接核价器
|
||||
* @param {string} ip - IP 地址
|
||||
* @param {string} port - 端口
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const testConnection = (ip, port) => {
|
||||
const params = new URLSearchParams()
|
||||
params.append('isbn', '0')
|
||||
params.append('out_id', '0')
|
||||
params.append('quality', '0')
|
||||
params.append('query_index', '1')
|
||||
params.append('user_id', '0')
|
||||
return axios.post(`http://${ip}:${port}/api/goods/query`, params.toString(), {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
timeout: 10000
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新价格到核价器
|
||||
* @param {string} ip - IP 地址
|
||||
* @param {string} port - 端口
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const saveNewPrice = (ip, port, newPrice, placeholderDownPrice, minShippingFee, minPrice, verifyIndex) => {
|
||||
const formData = new URLSearchParams()
|
||||
formData.append('new_price', newPrice)
|
||||
formData.append('placeholder_down_price', placeholderDownPrice)
|
||||
formData.append('min_shipping_fee', minShippingFee)
|
||||
formData.append('min_price', minPrice)
|
||||
formData.append('query_index', verifyIndex)
|
||||
console.log(formData.toString())
|
||||
return axios.post(`http://${ip}:${port}/api/config/price/set`, formData.toString(), {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
timeout: 10000
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 孔夫子旧书网登录(直接请求核价器)
|
||||
* @param {string} username - 孔网用户名
|
||||
* @param {string} password - 孔网密码
|
||||
* @param {string} ip - 核价器 IP
|
||||
* @param {string} port - 核价器端口
|
||||
* @returns {Promise<{code: number, data: {token: string, nickname: string}, message: string}>}
|
||||
*/
|
||||
export const kongfzLogin = (username, password, ip, port) => {
|
||||
const formData = new FormData()
|
||||
formData.append('username', username)
|
||||
formData.append('password', password)
|
||||
return axios.post(`http://${ip}:${port}/api/kfz/login`, formData, {
|
||||
timeout: 15000
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量提交 Token 到核价器
|
||||
* @param {Array<{username: string, token: string}>} tokens - 账号 Token 列表
|
||||
* @param {string} ip - 核价器 IP
|
||||
* @param {string} port - 核价器端口
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const batchAddTokens = (tokens, ip, port) => {
|
||||
return axios.post(`http://${ip}:${port}/api/token/add`, tokens, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 10000
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从核价器获取已保存的 Token 列表
|
||||
* @param {string} ip - 核价器 IP
|
||||
* @param {string} port - 核价器端口
|
||||
* @returns {Promise<{code: number, data: Array<{Username: string, Token: string, ID: number, IsEnable: boolean}>, message: string}>}
|
||||
*/
|
||||
export const fetchTokenList = (ip, port) => {
|
||||
return axios.post(`http://${ip}:${port}/api/token/list`, {}, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 10000
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从核价器获取config配置
|
||||
* @param {string} ip - 核价器 IP
|
||||
* @param {string} port - 核价器端口
|
||||
*/
|
||||
export const fetchConfig = (ip, port) => {
|
||||
return axios.post(`http://${ip}:${port}/api/config/price/get`, {}, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 10000
|
||||
}).then(res => res.data)
|
||||
}
|
||||
968
backups/config.vue.bak
Normal file
968
backups/config.vue.bak
Normal file
@ -0,0 +1,968 @@
|
||||
<template>
|
||||
<div class="config-page">
|
||||
<el-card class="config-card" shadow="always">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<el-icon :size="20">
|
||||
<Setting />
|
||||
</el-icon>
|
||||
<span>核价器配置</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form label-width="80px" label-position="top">
|
||||
<div class="config-row">
|
||||
<el-form-item class="flex-item">
|
||||
<template #label>
|
||||
<span class="label-with-icon">
|
||||
<span>程序所在位置</span>
|
||||
<el-tooltip content="verifyTool的位置" placement="top" trigger="click">
|
||||
<el-icon style="cursor: pointer;">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="dir" placeholder="如 C:\\verifyTool" clearable @clear="dir = ''" />
|
||||
<el-tooltip content="通过自定义协议启动本机程序" placement="top" trigger="click">
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="VideoPlay"
|
||||
style="margin-left: 10px"
|
||||
:disabled="!dir"
|
||||
@click="handleOpenExe"
|
||||
>
|
||||
打开程序
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<!-- IP地址 -->
|
||||
<el-form-item class="flex-item">
|
||||
<template #label>
|
||||
<span class="label-with-icon">
|
||||
<span>IP 地址</span>
|
||||
<el-tooltip content="一般都是 127.0.0.1 如需特殊配置 请联系网管" placement="top" trigger="click">
|
||||
<el-icon style="cursor: pointer;">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="ip" placeholder="如 192.168.1.1" clearable />
|
||||
</el-form-item>
|
||||
<!-- 端口 -->
|
||||
<el-form-item class="flex-item">
|
||||
<template #label>
|
||||
<span class="label-with-icon">
|
||||
<span>端口</span>
|
||||
<el-tooltip content="默认是8080 但是由于每台电脑的环境都不同 可能会出现端口冲突 在出现问题时候 请第一时间联系网管" placement="top" trigger="click">
|
||||
<el-icon style="cursor: pointer;">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="port" placeholder="如 8080" clearable />
|
||||
</el-form-item>
|
||||
<!-- 核价位置 -->
|
||||
<el-form-item class="flex-item">
|
||||
<template #label>
|
||||
<span class="label-with-icon">
|
||||
<span>核价位置</span>
|
||||
<el-tooltip content="默认 1 根据实际情况自行填写" placement="top" trigger="click">
|
||||
<el-icon style="cursor: pointer;">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="verifyIndex" placeholder="请输入核价位置" clearable />
|
||||
</el-form-item>
|
||||
<!-- 核价失败默认价格 -->
|
||||
<el-form-item class="flex-item">
|
||||
<template #label>
|
||||
<span class="label-with-icon">
|
||||
<span>核价失败统一默认价格(元)</span>
|
||||
<el-tooltip content="例:88888 便于再店铺中快速搜索到这些商品 商品修改价格" placement="top" trigger="click">
|
||||
<el-icon style="cursor: pointer;">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="newPrice" placeholder="请输入核价失败时的默认价格" clearable @input="e => onPriceInput(e, 'newPrice')"
|
||||
@blur="onPriceBlur('newPrice')" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<!-- 占位降价 -->
|
||||
<el-form-item class="flex-item">
|
||||
<template #label>
|
||||
<span class="label-with-icon">
|
||||
<span>占位降价(元)</span>
|
||||
<el-tooltip content="占位降价金额" placement="top" trigger="click">
|
||||
<el-icon style="cursor: pointer;">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="placeholderDownPrice" placeholder="请输入占位降价" clearable
|
||||
@input="e => onPriceInput(e, 'placeholderDownPrice')"
|
||||
@blur="enforceMinPlaceholderDownPrice(); onPriceBlur('placeholderDownPrice')" />
|
||||
</el-form-item>
|
||||
<!-- 最低运费 -->
|
||||
<el-form-item class="flex-item">
|
||||
<template #label>
|
||||
<span class="label-with-icon">
|
||||
<span>最低运费(元)</span>
|
||||
<el-tooltip content="订单最低运费金额" placement="top" trigger="click">
|
||||
<el-icon style="cursor: pointer;">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="minShippingFee" placeholder="请输入最低运费" clearable
|
||||
@input="e => onPriceInput(e, 'minShippingFee')" @blur="onPriceBlur('minShippingFee')" />
|
||||
</el-form-item>
|
||||
<!-- 最低书价 -->
|
||||
<el-form-item class="flex-item">
|
||||
<template #label>
|
||||
<span class="label-with-icon">
|
||||
<span>最低书价(元)</span>
|
||||
<el-tooltip content="单本书籍最低售价" placement="top" trigger="click">
|
||||
<el-icon style="cursor: pointer;">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="minPrice" placeholder="请输入最低书价" clearable @input="e => onPriceInput(e, 'minPrice')"
|
||||
@blur="onPriceBlur('minPrice')" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="save-bar">
|
||||
<el-tooltip content="测试核价器服务连接状态" placement="top" trigger="click">
|
||||
<el-button type="primary" :loading="testLoading" @click="handleTest" size="small">测试连接</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="保存当前核价器配置" placement="top" trigger="click">
|
||||
<el-button type="success" @click="handleSave">保存配置</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="result" class="result-box">
|
||||
<el-alert :title="result.success ? '✅ 连接成功' : '❌ 连接失败'" :type="result.success ? 'success' : 'error'"
|
||||
:description="result.message" show-icon :closable="false" />
|
||||
</div>
|
||||
</transition>
|
||||
</el-form>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<!-- 账号管理 -->
|
||||
<div class="account-section">
|
||||
<div class="section-title">
|
||||
<el-icon :size="18">
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>绑定孔夫子旧书网账号</span>
|
||||
<el-tooltip content="这里是为了波次提交之后店铺同步商品时候有一个相对准确市场价格(如果没有价格我们将走设置的统一默认价格)" placement="top" trigger="click">
|
||||
<el-icon style="cursor: pointer; margin-left: 8px;">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 有数据 → 表格 + 追加账号按钮 -->
|
||||
<template v-if="savedAccountList.length > 0">
|
||||
<el-table :data="savedAccountList" border stripe size="small" style="width: 100%">
|
||||
<el-table-column prop="username" label="用户名" min-width="140" align="center" />
|
||||
<el-table-column prop="token" label="Token" min-width="260" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="token-text">{{ row.token }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip content="删除该账号绑定" placement="top" trigger="click">
|
||||
<el-button type="danger" link :icon="Delete" @click="deleteSavedAccount(row)">删除</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 追加账号按钮 -->
|
||||
<div class="append-bar">
|
||||
<el-button v-if="!showAppendForm" type="primary" plain :icon="Plus" @click="showAppendForm = true">
|
||||
追加账号
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 追加账号输入表单 -->
|
||||
<transition name="fade">
|
||||
<div v-if="showAppendForm" class="account-form">
|
||||
<div v-for="(item, index) in appendAccounts" :key="index" class="account-row">
|
||||
<div class="account-label">
|
||||
追加账号{{ ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] || (index + 1) }}
|
||||
</div>
|
||||
<div class="account-inputs">
|
||||
<!-- 账号输入框 -->
|
||||
<el-form-item :label-width="'0'" style="margin-bottom: 0;">
|
||||
<template #label>
|
||||
<span class="label-with-icon" style="display: none;"></span>
|
||||
</template>
|
||||
<el-input v-model="item.account" placeholder="请输入账号" clearable autocomplete="off" />
|
||||
</el-form-item>
|
||||
<!-- 密码输入框 -->
|
||||
<el-form-item :label-width="'0'" style="margin-bottom: 0;">
|
||||
<el-input v-model="item.password" type="password" placeholder="请输入密码" clearable show-password
|
||||
autocomplete="new-password" />
|
||||
</el-form-item>
|
||||
<el-tooltip v-if="appendAccounts.length > 1" content="删除此条账号输入" placement="top" trigger="click">
|
||||
<el-button type="danger" :icon="Delete" circle
|
||||
@click="removeAppendAccount(index)" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="append-form-actions">
|
||||
<el-tooltip content="再增加一组账号密码输入" placement="top" trigger="click">
|
||||
<el-button plain :icon="Plus" @click="addAppendAccount">添加更多</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="绑定当前填写的所有追加账号" placement="top" trigger="click">
|
||||
<el-button type="primary" :loading="appendLoading" :icon="Link"
|
||||
@click="handleAppendBind">绑定追加账号</el-button>
|
||||
</el-tooltip>
|
||||
<el-button @click="cancelAppend">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<!-- 无数据 → 输入表单 -->
|
||||
<template v-else>
|
||||
<div class="account-form">
|
||||
<div v-for="(item, index) in accounts" :key="index" class="account-row">
|
||||
<div class="account-label">账号{{ ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] }}</div>
|
||||
<div class="account-inputs">
|
||||
<el-input v-model="item.account" placeholder="请输入账号" clearable autocomplete="off" />
|
||||
<el-input v-model="item.password" type="password" placeholder="请输入密码" clearable show-password
|
||||
autocomplete="new-password" />
|
||||
<el-tooltip v-if="accounts.length > 1" content="删除此条账号输入" placement="top" trigger="click">
|
||||
<el-button type="danger" :icon="Delete" circle
|
||||
@click="removeAccount(index)" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<el-tooltip content="再增加一组账号密码输入" placement="top" trigger="click">
|
||||
<el-button type="primary" plain :icon="Plus" @click="addAccount" class="add-btn">添加更多账号</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 绑定账号按钮(无已绑定账号时显示) -->
|
||||
<div v-if="savedAccountList.length === 0" class="bind-bar">
|
||||
<el-tooltip content="绑定新的核价器账号" placement="top" trigger="click">
|
||||
<el-button type="primary" :loading="bindLoading" :icon="Link" @click="handleBind">绑定账号</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Plus, Setting, User, Link, VideoPlay } from '@element-plus/icons-vue'
|
||||
import {
|
||||
testConnection,
|
||||
kongfzLogin,
|
||||
batchAddTokens,
|
||||
fetchTokenList,
|
||||
saveNewPrice,
|
||||
fetchConfig,
|
||||
} from '@/api/config'
|
||||
|
||||
const STORAGE_KEY_FILE_DIR = 'file_dir'
|
||||
const STORAGE_KEY_IP = 'test_ip'
|
||||
const STORAGE_KEY_PORT = 'test_port'
|
||||
const STORAGE_KEY_VERIFY_INDEX = 'verify_index'
|
||||
const STORAGE_KEY_SAVED_ACCOUNTS = 'saved_accounts'
|
||||
|
||||
interface AccountEntry {
|
||||
id?: number
|
||||
account: string
|
||||
token: string
|
||||
username: string
|
||||
}
|
||||
|
||||
interface FormAccount {
|
||||
account: string
|
||||
password: string
|
||||
token?: string
|
||||
username?: string
|
||||
}
|
||||
|
||||
/** 从 localStorage 读取已保存的账号列表 */
|
||||
function loadSavedAccounts(): AccountEntry[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY_SAVED_ACCOUNTS)
|
||||
return raw ? JSON.parse(raw) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** 持久化已保存的账号列表到 localStorage */
|
||||
function saveSavedAccounts(list: AccountEntry[]): void {
|
||||
localStorage.setItem(STORAGE_KEY_SAVED_ACCOUNTS, JSON.stringify(list))
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Config',
|
||||
setup() {
|
||||
// 从本地缓存读取初始值
|
||||
const dir = ref(localStorage.getItem(STORAGE_KEY_FILE_DIR) || '')
|
||||
const ip = ref(localStorage.getItem(STORAGE_KEY_IP) || '127.0.0.1')
|
||||
const port = ref(localStorage.getItem(STORAGE_KEY_PORT) || '8080')
|
||||
const verifyIndex = ref(localStorage.getItem(STORAGE_KEY_VERIFY_INDEX) || '3')
|
||||
const fmt = (v: string | null, d: string) => { const n = parseFloat(v ?? ''); return isNaN(n) ? d : n.toFixed(2) }
|
||||
const newPrice = ref(fmt(localStorage.getItem('new_price'), '9999.00'))
|
||||
const placeholderDownPrice = ref(fmt(localStorage.getItem('placeholder_down_price'), '0.01'))
|
||||
const minShippingFee = ref(fmt(localStorage.getItem('min_shipping_fee'), '3.00'))
|
||||
const minPrice = ref(fmt(localStorage.getItem('min_price'), '1.00'))
|
||||
// 账号密码组(支持多个)
|
||||
const accounts = ref<FormAccount[]>([{ account: '', password: '', token: '', username: '' }])
|
||||
// 已保存的账号 Token 列表(不含密码)
|
||||
const savedAccountList = ref<AccountEntry[]>([])
|
||||
// 追加账号相关
|
||||
const showAppendForm = ref(false)
|
||||
const appendAccounts = ref<FormAccount[]>([{ account: '', password: '', token: '', username: '' }])
|
||||
const appendLoading = ref(false)
|
||||
/** 添加新的账号输入行 */
|
||||
const addAccount = () => {
|
||||
accounts.value.push({ account: '', password: '', token: '', username: '' })
|
||||
}
|
||||
/** 删除账号输入行 */
|
||||
const removeAccount = (index: number) => {
|
||||
accounts.value.splice(index, 1)
|
||||
}
|
||||
/** 添加新的追加账号输入行 */
|
||||
const addAppendAccount = () => {
|
||||
appendAccounts.value.push({ account: '', password: '', token: '', username: '' })
|
||||
}
|
||||
/** 删除追加账号输入行 */
|
||||
const removeAppendAccount = (index: number) => {
|
||||
appendAccounts.value.splice(index, 1)
|
||||
}
|
||||
/** 取消追加账号操作,重置表单 */
|
||||
const cancelAppend = () => {
|
||||
showAppendForm.value = false
|
||||
appendAccounts.value = [{ account: '', password: '', token: '', username: '' }]
|
||||
}
|
||||
/** 占位降价失焦时强制最低值为 0.01 */
|
||||
const enforceMinPlaceholderDownPrice = () => {
|
||||
const val = parseFloat(placeholderDownPrice.value)
|
||||
if (isNaN(val) || val < 0.01) {
|
||||
placeholderDownPrice.value = '0.01'
|
||||
ElMessage.warning('占位降价不能低于 0.01,已自动设为 0.01')
|
||||
}
|
||||
}
|
||||
/** 金额输入过滤:只允许数字+小数点,最多2位小数,保持光标位置 */
|
||||
const onPriceInput = (value: string, key: string) => {
|
||||
const raw = value
|
||||
const filtered = raw
|
||||
.replace(/[^\d.]/g, '') // 只保留数字和小数点
|
||||
.replace(/^\./, '0.') // 以点开头补0
|
||||
.replace(/\.{2,}/g, '.') // 去重小数点
|
||||
.replace(/^(\d+\.?\d{0,2}).*$/, '$1') // 最多2位小数
|
||||
const map: Record<string, any> = { newPrice, placeholderDownPrice, minShippingFee, minPrice }
|
||||
map[key].value = filtered
|
||||
}
|
||||
|
||||
/** 失焦时自动补全 .00:纯整数 → 末尾补 .00 */
|
||||
const onPriceBlur = (key: string) => {
|
||||
const map: Record<string, any> = { newPrice, placeholderDownPrice, minShippingFee, minPrice }
|
||||
const val = map[key].value as string
|
||||
// 只对纯数字(不含小数点)补 .00
|
||||
if (val && /^\d+$/.test(val)) {
|
||||
map[key].value = val + '.00'
|
||||
} else if (val && /^\d+\.$/.test(val)) {
|
||||
// 如果结尾是小数点,如 "12." → 补成 "12.00"
|
||||
map[key].value = val + '00'
|
||||
} else if (val && /^\d+\.\d$/.test(val)) {
|
||||
// 如果只有一位小数,如 "12.3" → 补成 "12.30"
|
||||
map[key].value = val + '0'
|
||||
}
|
||||
}
|
||||
|
||||
const testLoading = ref(false)
|
||||
const bindLoading = ref(false)
|
||||
const result = ref<{ success: boolean; message: string } | null>(null)
|
||||
|
||||
/** 通过自定义协议启动本地 kfz-goods-pricing.exe */
|
||||
const handleOpenExe = () => {
|
||||
if (!dir.value) {
|
||||
ElMessage.warning('请先设置程序所在位置')
|
||||
return
|
||||
}
|
||||
try {
|
||||
// 调用自定义协议 kfzgs://,launcher.exe 会读取 dir 并启动目标 exe
|
||||
window.location.href = `kfzgs://launch?dir=${encodeURIComponent(dir.value)}`
|
||||
} catch (err: any) {
|
||||
ElMessage.error(`启动失败: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** 将单个账号写入已保存列表 */
|
||||
const addToSavedList = (entry: AccountEntry) => {
|
||||
const list = loadSavedAccounts()
|
||||
const idx = list.findIndex(a => a.account === entry.account)
|
||||
if (idx >= 0) {
|
||||
list[idx] = entry
|
||||
} else {
|
||||
list.push(entry)
|
||||
}
|
||||
saveSavedAccounts(list)
|
||||
savedAccountList.value = list
|
||||
}
|
||||
|
||||
/** 删除已保存的账号 */
|
||||
const deleteSavedAccount = async (entry: AccountEntry) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除账号「${entry.account}」的 Token 记录吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
// 调用删除接口
|
||||
await axios.get(`http://${ip.value}:${port.value}/api/token/delete`, {
|
||||
params: { id: entry.id }
|
||||
})
|
||||
// 同时清除 username 对应的 localStorage key
|
||||
localStorage.removeItem(entry.username)
|
||||
const list = loadSavedAccounts().filter(a => a.account !== entry.account)
|
||||
saveSavedAccounts(list)
|
||||
savedAccountList.value = list
|
||||
ElMessage.success('已删除')
|
||||
} catch {
|
||||
// 取消删除不做任何操作
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载已保存的账号列表 */
|
||||
const loadSavedAccountsList = () => {
|
||||
savedAccountList.value = loadSavedAccounts()
|
||||
}
|
||||
|
||||
/** 测试核价器连接 */
|
||||
const handleTest = async () => {
|
||||
if (!ip.value) {
|
||||
ElMessage.warning('请输入 IP 地址')
|
||||
return
|
||||
}
|
||||
if (!port.value) {
|
||||
ElMessage.warning('请输入端口号')
|
||||
return
|
||||
}
|
||||
|
||||
testLoading.value = true
|
||||
result.value = null
|
||||
|
||||
try {
|
||||
const testRes = await testConnection(ip.value, port.value)
|
||||
if (testRes.code === 200) {
|
||||
result.value = {
|
||||
success: true,
|
||||
message: testRes.message
|
||||
}
|
||||
} else {
|
||||
result.value = {
|
||||
success: false,
|
||||
message: testRes.message
|
||||
}
|
||||
}
|
||||
console.log('测试结果:', testRes)
|
||||
} catch (error: any) {
|
||||
let message = '未知错误'
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
message = '连接超时,请检查地址是否正确或联系服务供应方'
|
||||
} else if (error.message) {
|
||||
message = error.message
|
||||
}
|
||||
|
||||
result.value = {
|
||||
success: false,
|
||||
message
|
||||
}
|
||||
console.log('测试失败:', error)
|
||||
} finally {
|
||||
testLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 绑定账号:登录孔网 + 批量提交 Token 到核价器 + 刷新列表 */
|
||||
const handleBind = async () => {
|
||||
if (!ip.value) {
|
||||
ElMessage.warning('请先填写核价器 IP 地址')
|
||||
return
|
||||
}
|
||||
if (!port.value) {
|
||||
ElMessage.warning('请先填写核价器端口号')
|
||||
return
|
||||
}
|
||||
|
||||
bindLoading.value = true
|
||||
|
||||
try {
|
||||
// 依次登录每个已填写的账号
|
||||
const successfulAccounts: { username: string; token: string }[] = []
|
||||
for (let i = 0; i < accounts.value.length; i++) {
|
||||
const item = accounts.value[i]
|
||||
if (item.account && item.password) {
|
||||
try {
|
||||
const loginRes = await kongfzLogin(item.account, item.password, ip.value, port.value)
|
||||
console.log('核价器登录响应:', loginRes)
|
||||
if (loginRes?.code === 200 && loginRes?.data) {
|
||||
const { token, nickname: username } = loginRes.data
|
||||
accounts.value[i].token = token
|
||||
accounts.value[i].username = username
|
||||
localStorage.setItem(username, token)
|
||||
addToSavedList({
|
||||
account: item.account,
|
||||
username,
|
||||
token
|
||||
})
|
||||
successfulAccounts.push({ username, token })
|
||||
console.log(`账号 ${item.account} 登录成功,username: ${username}`)
|
||||
} else {
|
||||
console.log(`账号 ${item.account} 登录失败:`, loginRes?.message)
|
||||
}
|
||||
} catch (loginError: any) {
|
||||
console.log(`账号 ${item.account} 登录失败:`, loginError.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将成功的 Token 批量提交到核价器
|
||||
if (successfulAccounts.length > 0) {
|
||||
try {
|
||||
const addRes = await batchAddTokens(successfulAccounts, ip.value, port.value)
|
||||
console.log('token/add 响应:', addRes)
|
||||
} catch (addError: any) {
|
||||
// console.error('token/add 请求失败:', addError.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 从服务器重新获取完整列表,同步本地
|
||||
try {
|
||||
const res = await fetchTokenList(ip.value, port.value)
|
||||
const tokenList: { Username: string; Token: string; ID: number; IsEnable: boolean }[] = res?.data
|
||||
if (Array.isArray(tokenList)) {
|
||||
const entries: AccountEntry[] = tokenList
|
||||
.filter(t => t.IsEnable)
|
||||
.map(t => ({
|
||||
id: t.ID,
|
||||
account: t.Username,
|
||||
username: t.Username,
|
||||
token: t.Token
|
||||
}))
|
||||
saveSavedAccounts(entries)
|
||||
savedAccountList.value = entries
|
||||
entries.forEach(e => localStorage.setItem(e.username, e.token))
|
||||
}
|
||||
} catch (fetchErr) {
|
||||
// console.error('获取已保存 Token 列表失败:', fetchErr)
|
||||
}
|
||||
|
||||
if (successfulAccounts.length > 0) {
|
||||
ElMessage.success(`成功绑定 ${successfulAccounts.length} 个账号`)
|
||||
} else {
|
||||
ElMessage.info('没有成功绑定的账号,请检查账号密码是否正确')
|
||||
}
|
||||
} finally {
|
||||
bindLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 追加账号绑定:与 handleBind 逻辑一致,但操作 appendAccounts */
|
||||
const handleAppendBind = async () => {
|
||||
if (!ip.value) {
|
||||
ElMessage.warning('请先填写核价器 IP 地址')
|
||||
return
|
||||
}
|
||||
if (!port.value) {
|
||||
ElMessage.warning('请先填写核价器端口号')
|
||||
return
|
||||
}
|
||||
|
||||
appendLoading.value = true
|
||||
|
||||
try {
|
||||
const successfulAccounts: { username: string; token: string }[] = []
|
||||
for (let i = 0; i < appendAccounts.value.length; i++) {
|
||||
const item = appendAccounts.value[i]
|
||||
if (item.account && item.password) {
|
||||
try {
|
||||
const loginRes = await kongfzLogin(item.account, item.password, ip.value, port.value)
|
||||
if (loginRes?.code === 200 && loginRes?.data) {
|
||||
const { token, nickname: username } = loginRes.data
|
||||
appendAccounts.value[i].token = token
|
||||
appendAccounts.value[i].username = username
|
||||
localStorage.setItem(username, token)
|
||||
addToSavedList({
|
||||
account: item.account,
|
||||
username,
|
||||
token
|
||||
})
|
||||
successfulAccounts.push({ username, token })
|
||||
}
|
||||
} catch (loginError: any) {
|
||||
console.log(`追加账号 ${item.account} 登录失败:`, loginError.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (successfulAccounts.length > 0) {
|
||||
try {
|
||||
await batchAddTokens(successfulAccounts, ip.value, port.value)
|
||||
} catch (addError: any) {
|
||||
// console.error('追加 token/add 请求失败:', addError.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 从服务器重新获取完整列表,同步本地
|
||||
try {
|
||||
const res = await fetchTokenList(ip.value, port.value)
|
||||
const tokenList: { Username: string; Token: string; ID: number; IsEnable: boolean }[] = res?.data
|
||||
if (Array.isArray(tokenList)) {
|
||||
const entries: AccountEntry[] = tokenList
|
||||
.filter(t => t.IsEnable)
|
||||
.map(t => ({
|
||||
id: t.ID,
|
||||
account: t.Username,
|
||||
username: t.Username,
|
||||
token: t.Token
|
||||
}))
|
||||
saveSavedAccounts(entries)
|
||||
savedAccountList.value = entries
|
||||
entries.forEach(e => localStorage.setItem(e.username, e.token))
|
||||
}
|
||||
} catch (fetchErr) {
|
||||
// console.error('获取已保存 Token 列表失败:', fetchErr)
|
||||
}
|
||||
|
||||
if (successfulAccounts.length > 0) {
|
||||
ElMessage.success(`成功追加绑定 ${successfulAccounts.length} 个账号`)
|
||||
} else {
|
||||
ElMessage.info('没有成功绑定的账号,请检查账号密码是否正确')
|
||||
}
|
||||
|
||||
// 重置追加表单
|
||||
showAppendForm.value = false
|
||||
appendAccounts.value = [{ account: '', password: '', token: '', username: '' }]
|
||||
} finally {
|
||||
appendLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存配置 */
|
||||
const handleSave = async () => {
|
||||
if (!ip.value) {
|
||||
ElMessage.warning('请输入 IP 地址')
|
||||
return
|
||||
}
|
||||
if (!port.value) {
|
||||
ElMessage.warning('请输入端口号')
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem(STORAGE_KEY_FILE_DIR, dir.value)
|
||||
localStorage.setItem(STORAGE_KEY_IP, ip.value)
|
||||
localStorage.setItem(STORAGE_KEY_PORT, port.value)
|
||||
localStorage.setItem(STORAGE_KEY_VERIFY_INDEX, verifyIndex.value)
|
||||
localStorage.setItem('new_price', newPrice.value)
|
||||
localStorage.setItem('placeholder_down_price', placeholderDownPrice.value)
|
||||
localStorage.setItem('min_shipping_fee', minShippingFee.value)
|
||||
localStorage.setItem('min_price', minPrice.value)
|
||||
console.log('核价配置已保存:', { ip: ip.value, port: port.value, verifyIndex: verifyIndex.value })
|
||||
|
||||
const newpriceNum = parseFloat(newPrice.value)
|
||||
const placeholderDownPriceNum = parseFloat(placeholderDownPrice.value)
|
||||
const minShippingFeeNum = parseFloat(minShippingFee.value)
|
||||
const minPriceNum = parseFloat(minPrice.value)
|
||||
|
||||
if (!isNaN(newpriceNum) && newpriceNum >= 0) {
|
||||
const sendNewPrice = newpriceNum
|
||||
const sendPlaceholderDownPrice = !isNaN(placeholderDownPriceNum) && placeholderDownPriceNum >= 0
|
||||
? placeholderDownPriceNum
|
||||
: 0.01
|
||||
const sendMinShippingFee = !isNaN(minShippingFeeNum) && minShippingFeeNum >= 0
|
||||
? minShippingFeeNum
|
||||
: 3.00
|
||||
const sendMinPrice = !isNaN(minPriceNum) && minPriceNum >= 0
|
||||
? minPriceNum
|
||||
: 1.00
|
||||
try {
|
||||
await saveNewPrice(ip.value, port.value, sendNewPrice, sendPlaceholderDownPrice, sendMinShippingFee, sendMinPrice,verifyIndex.value)
|
||||
console.log('核价价格相关配置已保存')
|
||||
} catch (err: any) {
|
||||
console.log('核价价格相关配置保存失败:', err.message)
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.success(`配置已保存:IP=${ip.value}, PORT=${port.value}, 核价位置=${verifyIndex.value}`)
|
||||
}
|
||||
|
||||
/** 从服务器获取已保存的 Token 列表并同步到本地 */
|
||||
const fetchSavedTokens = async (): Promise<void> => {
|
||||
try {
|
||||
const res = await fetchTokenList(ip.value, port.value)
|
||||
const tokenList: { Username: string; Token: string; ID: number; IsEnable: boolean }[] = res?.data
|
||||
if (Array.isArray(tokenList) && tokenList.length > 0) {
|
||||
const entries: AccountEntry[] = tokenList
|
||||
.filter(t => t.IsEnable)
|
||||
.map(t => ({
|
||||
id: t.ID,
|
||||
account: t.Username,
|
||||
username: t.Username,
|
||||
token: t.Token
|
||||
}))
|
||||
saveSavedAccounts(entries)
|
||||
savedAccountList.value = entries
|
||||
// 同时将每个 token 写入单独的 localStorage key
|
||||
entries.forEach(e => localStorage.setItem(e.username, e.token))
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('获取已保存 Token 列表失败(首次使用可忽略):', err)
|
||||
}
|
||||
}
|
||||
|
||||
/** 从核价器拉取价格配置,补全 localStorage 中缺失的字段 */
|
||||
const loadPriceConfig = async (): Promise<void> => {
|
||||
try {
|
||||
const res = await fetchConfig(ip.value, port.value)
|
||||
const data = res?.data
|
||||
if (data) {
|
||||
// 响应字段均为元单位,无需转换
|
||||
const fieldMap: { respKey: string; storeKey: string }[] = [
|
||||
{ respKey: 'QueryIndex', storeKey: 'verify_index' },
|
||||
{ respKey: 'NewPrice', storeKey: 'new_price' },
|
||||
{ respKey: 'PlaceholderDownPrice', storeKey: 'placeholder_down_price' },
|
||||
{ respKey: 'MinShippingFee', storeKey: 'min_shipping_fee' },
|
||||
{ respKey: 'MinPrice', storeKey: 'min_price' },
|
||||
]
|
||||
console.log('从服务器获取的核价配置:', data)
|
||||
for (const f of fieldMap) {
|
||||
const val = (data as any)[f.respKey]
|
||||
if (val !== undefined && val !== null) {
|
||||
localStorage.setItem(f.storeKey, String(val))
|
||||
console.log(`loadPriceConfig: 同步 ${f.storeKey} = ${val}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 同步到页面 ref
|
||||
verifyIndex.value = localStorage.getItem(STORAGE_KEY_VERIFY_INDEX) || verifyIndex.value
|
||||
newPrice.value = fmt(localStorage.getItem('new_price'), '0.00')
|
||||
placeholderDownPrice.value = fmt(localStorage.getItem('placeholder_down_price'), '0.01')
|
||||
minShippingFee.value = fmt(localStorage.getItem('min_shipping_fee'), '3.00')
|
||||
minPrice.value = fmt(localStorage.getItem('min_price'), '1.00')
|
||||
|
||||
// 补上 Port(如果为空)
|
||||
if (!port.value && (data as any).Port) {
|
||||
port.value = String((data as any).Port)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('获取核价器配置失败(首次使用可忽略):', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时加载已保存的账号列表 + 从服务器同步 + 拉取价格配置
|
||||
onMounted(() => {
|
||||
loadSavedAccountsList()
|
||||
fetchSavedTokens()
|
||||
loadPriceConfig()
|
||||
})
|
||||
|
||||
return {
|
||||
dir,
|
||||
ip,
|
||||
port,
|
||||
verifyIndex,
|
||||
newPrice,
|
||||
placeholderDownPrice,
|
||||
minShippingFee,
|
||||
minPrice,
|
||||
accounts,
|
||||
addAccount,
|
||||
removeAccount,
|
||||
showAppendForm,
|
||||
appendAccounts,
|
||||
appendLoading,
|
||||
addAppendAccount,
|
||||
removeAppendAccount,
|
||||
cancelAppend,
|
||||
enforceMinPlaceholderDownPrice,
|
||||
onPriceInput,
|
||||
onPriceBlur,
|
||||
handleAppendBind,
|
||||
testLoading,
|
||||
bindLoading,
|
||||
result,
|
||||
savedAccountList,
|
||||
deleteSavedAccount,
|
||||
handleTest,
|
||||
handleBind,
|
||||
handleSave,
|
||||
handleOpenExe,
|
||||
Delete,
|
||||
Plus,
|
||||
Setting,
|
||||
User,
|
||||
Link,
|
||||
VideoPlay
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.config-page {
|
||||
padding: 20px;
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.config-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.config-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.flex-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.save-bar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* 账号区域 */
|
||||
.account-section {
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.count-tag {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.account-form {
|
||||
.account-row {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.account-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.account-inputs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.bind-bar {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.append-bar {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.append-form-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.token-text {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 640px) {
|
||||
.config-row {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.account-inputs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.label-with-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
369
backups/goodsPop_index.vue.bak
Normal file
369
backups/goodsPop_index.vue.bak
Normal file
@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<el-popover
|
||||
placement="right-start"
|
||||
:width="360"
|
||||
trigger="hover"
|
||||
:open-delay="500"
|
||||
:close-delay="100"
|
||||
:disabled="!isbn"
|
||||
@show="handleShow"
|
||||
@hide="handleHide"
|
||||
>
|
||||
<template #reference>
|
||||
<span class="isbn-popover-trigger" :class="{ 'is-loading': loading }">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="popover-loading">
|
||||
<el-icon class="is-loading" :size="24"><Loading /></el-icon>
|
||||
<span>正在查询书品信息...</span>
|
||||
</div>
|
||||
|
||||
<!-- 查询失败 -->
|
||||
<div v-else-if="error" class="popover-error">
|
||||
<el-icon :size="24" color="#e6a23c"><WarningFilled /></el-icon>
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 查询成功 → 展示书品信息 -->
|
||||
<div v-else-if="bookData" class="popover-content">
|
||||
<div class="popover-header">
|
||||
<span class="popover-isbn">{{ isbn }}</span>
|
||||
<span v-if="bookData.isSuit" class="suit-badge">套装书</span>
|
||||
</div>
|
||||
|
||||
<div class="popover-body">
|
||||
<!-- 左侧:封面图片 -->
|
||||
<div class="popover-cover">
|
||||
<img
|
||||
v-if="bookData.book_pic?.pddPath"
|
||||
:src="bookData.book_pic.pddPath"
|
||||
alt="封面"
|
||||
class="cover-image"
|
||||
/>
|
||||
<div v-else class="cover-placeholder">
|
||||
<el-icon :size="32"><Picture /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:书籍详情 -->
|
||||
<div class="popover-info">
|
||||
<div class="info-row">
|
||||
<span class="info-label">书名:</span>
|
||||
<span class="info-value info-value-name">{{ bookData.bookName || '未知' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">作者:</span>
|
||||
<span class="info-value">{{ bookData.author || '未知' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">出版社:</span>
|
||||
<span class="info-value">{{ bookData.publisher || '未知' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">出版时间:</span>
|
||||
<span class="info-value">{{ bookData.publishDate || '未知' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">装帧:</span>
|
||||
<span class="info-value">{{ bookData.binding || '未知' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">定价:</span>
|
||||
<span class="info-value info-value-price">¥{{ formatPrice(bookData.price) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">页数:</span>
|
||||
<span class="info-value">{{ bookData.pageCount ?? '未知' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">字数:</span>
|
||||
<span class="info-value">{{ bookData.wordCount ?? '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ISBN 为空 -->
|
||||
<div v-else class="popover-empty">
|
||||
<span>暂无ISBN</span>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Loading, WarningFilled, Picture } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
interface BookPic {
|
||||
localPath?: string
|
||||
pddPath?: string
|
||||
}
|
||||
|
||||
interface BookInfoResult {
|
||||
bookName: string
|
||||
author: string
|
||||
publisher: string
|
||||
publishDate: string
|
||||
binding: string
|
||||
price: number
|
||||
pageCount: number
|
||||
wordCount: number
|
||||
book_pic?: BookPic
|
||||
isSuit: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
/** ISBN 编号 */
|
||||
isbn?: string
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const bookData = ref<BookInfoResult | null>(null)
|
||||
|
||||
// 缓存已查询过的 ISBN,避免重复请求
|
||||
const cache = new Map<string, BookInfoResult>()
|
||||
|
||||
/** 格式化出版时间:处理年月日格式 */
|
||||
function formatPublishDate(value: string | number | undefined | null): string {
|
||||
if (value == null || value === '') return ''
|
||||
const str = String(value)
|
||||
// 如果已经是 yyyy-mm-dd 格式,直接返回
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return str
|
||||
// 如果是 yyyyMMdd 格式(8位数字)
|
||||
if (/^\d{8}$/.test(str)) {
|
||||
return `${str.slice(0, 4)}-${str.slice(4, 6)}-${str.slice(6, 8)}`
|
||||
}
|
||||
// 如果是 yyyy-mm 格式
|
||||
if (/^\d{4}-\d{2}$/.test(str)) return `${str}-01`
|
||||
// 如果是纯数字时间戳
|
||||
const num = Number(value)
|
||||
if (!isNaN(num) && num > 10000) {
|
||||
const d = new Date(num * 1000)
|
||||
if (!isNaN(d.getTime())) {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/** 格式化价格:分 → 元 */
|
||||
function formatPrice(priceInCents: number): string {
|
||||
if (priceInCents == null) return '0.00'
|
||||
return (priceInCents / 100).toFixed(2)
|
||||
}
|
||||
|
||||
async function fetchBookInfo(isbn: string) {
|
||||
// 检查缓存
|
||||
if (cache.has(isbn)) {
|
||||
bookData.value = cache.get(isbn)!
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
bookData.value = null
|
||||
|
||||
try {
|
||||
const payload = await request.get('/getBookInfo', {
|
||||
params: { isbn }
|
||||
})
|
||||
const data = payload?.data
|
||||
|
||||
if (!data) {
|
||||
error.value = '数据库中暂无该书数据'
|
||||
return
|
||||
}
|
||||
|
||||
const result: BookInfoResult = {
|
||||
bookName: data.book_name || '',
|
||||
author: data.author || '',
|
||||
publisher: data.publisher || '',
|
||||
publishDate: formatPublishDate(data.publication_time),
|
||||
binding: data.binding_layout || '',
|
||||
price: typeof data.fix_price === 'number' ? data.fix_price : 0,
|
||||
pageCount: Number(data.page_count) || 0,
|
||||
wordCount: Number(data.word_count) || 0,
|
||||
book_pic: data.book_pic || undefined,
|
||||
isSuit: data.is_suit === 1
|
||||
}
|
||||
|
||||
// 写入缓存
|
||||
cache.set(isbn, result)
|
||||
bookData.value = result
|
||||
} catch (err) {
|
||||
console.warn('[goodsPop] 书籍信息查询失败:', err instanceof Error ? err.message : String(err))
|
||||
error.value = '查询失败,请稍后重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleShow() {
|
||||
if (!props.isbn) return
|
||||
fetchBookInfo(props.isbn)
|
||||
}
|
||||
|
||||
function handleHide() {
|
||||
// 不做清理,保留上次查询结果以便下次快速展示
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.isbn-popover-trigger {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px dashed #409eff;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.isbn-popover-trigger:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
.isbn-popover-trigger.is-loading {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.popover-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 20px;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
.popover-loading .is-loading {
|
||||
animation: rotating 1.5s linear infinite;
|
||||
}
|
||||
@keyframes rotating {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.popover-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 20px;
|
||||
justify-content: center;
|
||||
color: #e6a23c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.popover-empty {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #c0c4cc;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 内容主体 */
|
||||
.popover-content {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.popover-isbn {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.suit-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
font-size: 11px;
|
||||
color: #e6a23c;
|
||||
background: #fdf6ec;
|
||||
border: 1px solid #f5dab1;
|
||||
border-radius: 4px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.popover-body {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* 封面 */
|
||||
.popover-cover {
|
||||
flex-shrink: 0;
|
||||
width: 90px;
|
||||
height: 120px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.cover-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f7fa;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
/* 详情 */
|
||||
.popover-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
flex-shrink: 0;
|
||||
color: #909399;
|
||||
width: 56px;
|
||||
text-align: right;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-value-name {
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.info-value-price {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
0
backups/reviewIllegalBook.js.bak
Normal file
0
backups/reviewIllegalBook.js.bak
Normal file
93
backups/shipping-order.js.bak
Normal file
93
backups/shipping-order.js.bak
Normal file
@ -0,0 +1,93 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** 发货单 API 基础路径 */
|
||||
const API_BASE = '/shipping-order'
|
||||
|
||||
/**
|
||||
* 标准化列表接口返回的数据格式
|
||||
* @param {Object} payload - 接口返回的原始响应对象
|
||||
* @returns {{ list: Array, total: number }} 标准化后的列表数据
|
||||
*/
|
||||
const normalizeListResponse = (payload) => {
|
||||
const data = payload?.data
|
||||
if (!data) return { list: [], total: 0 }
|
||||
if (Array.isArray(data)) return { list: data, total: data.length }
|
||||
return {
|
||||
list: Array.isArray(data.list) ? data.list : [],
|
||||
total: typeof data.total === 'number' ? data.total : 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取发货单列表(支持分页和筛选)
|
||||
* @param {Object} params - 请求参数
|
||||
* @param {string} [params.check_no] - 搜索关键字(发货单号)
|
||||
* @param {number} [params.status] - 状态筛选
|
||||
* @param {number} [params.customer_id] - 客户ID筛选
|
||||
* @param {number} [params.warehouse_id] - 仓库ID筛选
|
||||
* @param {number} [params.sales_order_id] - 销售订单ID筛选
|
||||
* @param {number} [params.wave_task_id] - 波次任务ID筛选
|
||||
* @param {number} [params.page] - 当前页码
|
||||
* @param {number} [params.pageSize] - 每页条数
|
||||
* @returns {Promise<{ list: Array, total: number }>} 标准化后的发货单列表
|
||||
*/
|
||||
export const fetchShippingOrderList = async ({ check_no, status, customer_id, warehouse_id, sales_order_id, wave_task_id, page, pageSize }) => {
|
||||
const params = {
|
||||
check_no: check_no || undefined,
|
||||
status,
|
||||
customer_id,
|
||||
warehouse_id,
|
||||
sales_order_id,
|
||||
wave_task_id,
|
||||
page,
|
||||
page_size: pageSize
|
||||
}
|
||||
const response = await request.get(`${API_BASE}/list`, { params })
|
||||
return normalizeListResponse(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个发货单详情(含明细行)
|
||||
* @param {string|number} id - 发货单ID
|
||||
* @returns {Promise<Object|null>} 发货单详情对象
|
||||
*/
|
||||
export const fetchShippingOrderDetail = async (id) => {
|
||||
const response = await request.get(`${API_BASE}/detail`, { params: { id } })
|
||||
return response?.data || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成发货单(form-data 格式)
|
||||
* @param {Object} params
|
||||
* @param {number} params.total - 选中的出库单数量
|
||||
* @param {number[]} params.outbound_order_ids - 选中的出库单 ID 数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const createShippingOrder = async ({ total, outbound_order_ids }) => {
|
||||
// 手动构建 FormData,按照 outbound_order_ids[0]={id} 的格式
|
||||
const formData = new FormData()
|
||||
formData.append('total', String(total))
|
||||
outbound_order_ids.forEach((id, index) => {
|
||||
formData.append(`order_ids[${index}]`, String(id))
|
||||
})
|
||||
return request.post(`${API_BASE}/create`, formData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新发货单物流信息(form-data 格式)
|
||||
* @param {Object} params
|
||||
* @param {number} params.shipping_order_id - 发货单ID
|
||||
* @param {number} params.sales_order_item_id - 销售订单明细ID
|
||||
* @param {string} params.logistics_company - 物流公司
|
||||
* @param {string} params.logistics_no - 物流单号
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const updateShippingOrderLogistics = async ({ shipping_order_id, sales_order_item_id, logistics_company, logistics_no }) => {
|
||||
const formData = new FormData()
|
||||
formData.append('shipping_order_id', String(shipping_order_id))
|
||||
formData.append('total', '1')
|
||||
formData.append('sales_order_item_id', String(sales_order_item_id))
|
||||
formData.append('logistics_company', logistics_company)
|
||||
formData.append('logistics_no', logistics_no)
|
||||
return request.post(`${API_BASE}/update`, formData)
|
||||
}
|
||||
50
src/api/book.js
Normal file
50
src/api/book.js
Normal file
@ -0,0 +1,50 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 根据 ISBN 查询书籍信息
|
||||
* @param {string} isbn - ISBN 编号
|
||||
* @returns {Promise<Object>} 接口返回的 Promise 对象,包含书籍信息
|
||||
*/
|
||||
export const getBookInfo = async (isbn) => {
|
||||
return request.get('/getBookInfo', { params: { isbn } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步/保存书籍信息
|
||||
* @param {Object} params - 书籍数据
|
||||
* @param {string} [params.book_name] - 书名
|
||||
* @param {string} [params.author] - 作者
|
||||
* @param {string} [params.publisher] - 出版社
|
||||
* @param {string} [params.publication_time] - 出版时间
|
||||
* @param {string} [params.binding_layout] - 装帧
|
||||
* @param {number} [params.fix_price] - 定价
|
||||
* @param {string} [params.isbn] - ISBN
|
||||
* @param {string|number} [params.page_count] - 页数
|
||||
* @param {string|number} [params.word_count] - 字数
|
||||
* @param {string} [params.book_format] - 开本
|
||||
* @param {number} [params.fid] - 上级ID
|
||||
* @param {string} [params.f_isbn] - 上级ISBN
|
||||
* @param {string} [params.f_book_name] - 上级书名
|
||||
* @param {string} [params.live_image] - 实拍图片
|
||||
* @param {string|number} [params.type] - 类型
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const syncBook = async (params) => {
|
||||
const body = {}
|
||||
if (params.book_name) body['book_name'] = params.book_name
|
||||
if (params.author) body['author'] = params.author
|
||||
if (params.publisher) body['publisher'] = params.publisher
|
||||
if (params.publication_time) body['publication_time'] = params.publication_time
|
||||
if (params.binding_layout) body['binding_layout'] = params.binding_layout
|
||||
if (params.fix_price !== undefined) body['fix_price'] = params.fix_price
|
||||
if (params.isbn) body['isbn'] = params.isbn
|
||||
if (params.page_count) body['page_count'] = params.page_count
|
||||
if (params.word_count) body['word_count'] = params.word_count
|
||||
if (params.book_format) body['book_format'] = params.book_format
|
||||
if (params.fid !== undefined) body['fid'] = params.fid
|
||||
if (params.f_isbn) body['f_isbn'] = params.f_isbn
|
||||
if (params.f_book_name) body['f_book_name'] = params.f_book_name
|
||||
if (params.live_image) body['live_image[0]'] = params.live_image
|
||||
if (params.type) body['type'] = params.type
|
||||
return request.post('/syncBook', body)
|
||||
}
|
||||
@ -94,4 +94,48 @@ export const fetchConfig = (ip, port) => {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 10000
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 向核价器查询商品信息
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} params.ip - 核价器 IP
|
||||
* @param {string} params.port - 核价器端口
|
||||
* @param {string} params.isbn - ISBN
|
||||
* @param {string|number} params.outId - 商品外部ID
|
||||
* @param {string|number} params.quality - 品质
|
||||
* @param {string|number} params.queryIndex - 查询索引
|
||||
* @param {string|number} params.userId - 用户ID
|
||||
* @param {string} [params.placeholderDownPrice] - 占位降价金额
|
||||
* @param {string} [params.minShippingFee] - 最低运费
|
||||
* @param {string} [params.minPrice] - 最低书价
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const queryGoodsPrice = ({ ip, port, isbn, outId, quality, queryIndex, userId, placeholderDownPrice, minShippingFee, minPrice }) => {
|
||||
const params = new URLSearchParams()
|
||||
params.append('isbn', isbn)
|
||||
params.append('out_id', String(outId))
|
||||
params.append('quality', String(quality))
|
||||
params.append('query_index', String(queryIndex))
|
||||
params.append('user_id', String(userId))
|
||||
if (placeholderDownPrice) params.append('placeholder_down_price', placeholderDownPrice)
|
||||
if (minShippingFee) params.append('min_shipping_fee', minShippingFee)
|
||||
if (minPrice) params.append('min_price', minPrice)
|
||||
return axios.post(`http://${ip}:${port}/api/goods/query`, params.toString(), {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
timeout: 10000
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除核价器中的 Token
|
||||
* @param {string} ip - 核价器 IP
|
||||
* @param {string} port - 核价器端口
|
||||
* @param {string|number} id - Token ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const deleteToken = (ip, port, id) => {
|
||||
return axios.get(`http://${ip}:${port}/api/token/delete`, {
|
||||
params: { id }
|
||||
}).then(res => res.data)
|
||||
}
|
||||
62
src/api/employee.js
Normal file
62
src/api/employee.js
Normal file
@ -0,0 +1,62 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取员工/代理列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} [params.page] - 当前页码
|
||||
* @param {number} [params.page_size] - 每页条数
|
||||
* @param {number|string} [params.status] - 状态筛选
|
||||
* @param {string} [params.keyword] - 搜索关键词(工号/姓名/账号)
|
||||
* @returns {Promise<{ code: number, data: { list: Array, total: number } }>}
|
||||
*/
|
||||
export const fetchEmployeeList = async (params) => {
|
||||
return request.get('/admin/employee/list', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加员工/代理
|
||||
* @param {Object} params - 添加参数
|
||||
* @param {string} params.name - 姓名
|
||||
* @param {string} params.password - 密码
|
||||
* @param {string} params.phone - 手机号
|
||||
* @param {number} [params.fid] - 上级ID
|
||||
* @param {number} [params.about_id] - about_id
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const addEmployee = async (params) => {
|
||||
return request.post('/admin/employee/add', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理积分充值
|
||||
* @param {string|number} employeeId - 员工ID
|
||||
* @param {Object} data - 充值数据
|
||||
* @param {number} data.amount - 充值数量
|
||||
* @param {string} [data.remark] - 备注
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const topupEmployee = async (employeeId, data) => {
|
||||
return request.post(`/admin/employee/topup/${employeeId}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理积分扣减
|
||||
* @param {string|number} employeeId - 员工ID
|
||||
* @param {Object} data - 扣减数据
|
||||
* @param {number} data.amount - 扣减数量
|
||||
* @param {string} [data.remark] - 备注
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const deductEmployee = async (employeeId, data) => {
|
||||
return request.post(`/admin/employee/deduct/${employeeId}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用代理
|
||||
* @param {'enable'|'disable'} action - 操作类型
|
||||
* @param {string|number} employeeId - 员工ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const toggleEmployeeStatus = async (action, employeeId) => {
|
||||
return request.post(`/admin/employee/${action}/${employeeId}`)
|
||||
}
|
||||
12
src/api/login.js
Normal file
12
src/api/login.js
Normal file
@ -0,0 +1,12 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param {'admin'|'employee'} type - 登录类型,'admin' 对应管理员(255),'employee' 对应代理(128)
|
||||
* @param {Object} formData - 表单数据 { username, password }
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const login = async (type, formData) => {
|
||||
const role = type === 'admin' ? '255' : '128'
|
||||
return request.post(`/login/${role}`, { ...formData, type: 1 })
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 标准化列表接口返回的数据格式
|
||||
*/
|
||||
const normalizeListResponse = (payload) => {
|
||||
const data = payload?.data
|
||||
if (!data) {
|
||||
return { list: [], total: 0 }
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
return { list: data, total: data.length }
|
||||
}
|
||||
return {
|
||||
list: Array.isArray(data.list) ? data.list : [],
|
||||
total: typeof data.total === 'number' ? data.total : Array.isArray(data.list) ? data.list.length : 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取书目异常记录列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} [params.barcode] - ISBN
|
||||
* @param {number|string} [params.status] - 状态筛选
|
||||
* @param {number} [params.page] - 当前页码
|
||||
* @param {number} [params.page_size] - 每页条数
|
||||
* @returns {Promise<{ list: Array, total: number }>}
|
||||
*/
|
||||
export const fetchProductLogList = async (params) => {
|
||||
const res = await request.get('/product_log/list', { params })
|
||||
const data = res?.data
|
||||
return {
|
||||
list: Array.isArray(data?.list) ? data.list : Array.isArray(data) ? data : [],
|
||||
total: data?.total || 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除书目异常记录
|
||||
* @param {string|number} id - 记录ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const deleteProductLog = async (id) => {
|
||||
const formData = new FormData()
|
||||
formData.append('id', String(id))
|
||||
return request.post('/product_log/delete', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
}
|
||||
@ -1,8 +1,13 @@
|
||||
import request from '@/utils/request'
|
||||
import axios from 'axios'
|
||||
|
||||
/** 发货单 API 基础路径 */
|
||||
const API_BASE = '/shipping-order'
|
||||
|
||||
// 外部 API 基础地址
|
||||
const BZY_API_BASE = 'https://api.buzhiyushu.cn'
|
||||
const PRINT_API_BASE = 'https://print.buzhiyushu.cn'
|
||||
|
||||
/**
|
||||
* 标准化列表接口返回的数据格式
|
||||
* @param {Object} payload - 接口返回的原始响应对象
|
||||
@ -91,3 +96,76 @@ export const updateShippingOrderLogistics = async ({ shipping_order_id, sales_or
|
||||
formData.append('logistics_no', logistics_no)
|
||||
return request.post(`${API_BASE}/update`, formData)
|
||||
}
|
||||
|
||||
// ───────── 外部快递/打印相关 API ─────────
|
||||
|
||||
/**
|
||||
* 获取快递账号列表(外部接口)
|
||||
* @param {string|number} shopId - 店铺ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const fetchFastMailList = async (shopId) => {
|
||||
const res = await axios.get(`${BZY_API_BASE}/zhishu/fastMail/listApi`, {
|
||||
params: { shopId }
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建快递面单(外部打印接口)
|
||||
* @param {Object} params - 面单参数
|
||||
* @param {string} params.type - 快递类型
|
||||
* @param {string} params.partnerId - 合作方ID
|
||||
* @param {string} params.secret - 密钥
|
||||
* @param {string} params.orderSn - 平台单号
|
||||
* @param {string} params.contact - 联系人
|
||||
* @param {string} params.phoneNumber - 联系电话
|
||||
* @param {string} params.province - 省份
|
||||
* @param {string} params.city - 城市
|
||||
* @param {string} params.area - 区县
|
||||
* @param {string} params.town - 详细地址
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const createOrderBatch = async (params) => {
|
||||
const formData = new FormData()
|
||||
formData.append('type', params.type)
|
||||
formData.append('partnerId', params.partnerId)
|
||||
formData.append('secret', params.secret)
|
||||
formData.append('orderSn', params.orderSn)
|
||||
formData.append('contact', params.contact)
|
||||
formData.append('phoneNumber', params.phoneNumber)
|
||||
formData.append('province', params.province)
|
||||
formData.append('city', params.city)
|
||||
formData.append('area', params.area)
|
||||
formData.append('town', params.town)
|
||||
const res = await axios.post(`${PRINT_API_BASE}/api/print/createOrderBatch`, formData)
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取打印面单信息(外部打印接口)
|
||||
* @param {Object} params - 参数
|
||||
* @param {string} params.mailno - 快递单号
|
||||
* @param {string} params.partnerId - 合作方ID
|
||||
* @param {string} params.secret - 密钥
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const createBmOrderDaYin = async (params) => {
|
||||
const res = await axios.get(`${PRINT_API_BASE}/api/print/createBmOrderDaYin`, { params })
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 回填快递单号(外部接口)
|
||||
* @param {Object} params - 参数
|
||||
* @param {string} params.code - 快递公司编码
|
||||
* @param {string} params.orderNo - 快递单号
|
||||
* @param {string} params.erpOrderId - ERP 订单ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const submitCompanyOrder = async (params) => {
|
||||
const res = await axios.post(`${BZY_API_BASE}/zhishu/orderExternalGoods/submitCompanyOrder`, params, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
@ -136,3 +136,12 @@ export const createWaveOutboundRelease = async (relatedOrderId, waveId) => {
|
||||
formData.append('wave_id', waveId)
|
||||
return request.post('/wave/outbound/release', formData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询波次状态
|
||||
* @param {string|number} id
|
||||
* @returns
|
||||
*/
|
||||
export const getWaveStatusById = async (id) => {
|
||||
return request.get(`/wave/getWaveStatusById?id=${id}`)
|
||||
}
|
||||
@ -26,7 +26,7 @@
|
||||
</template>
|
||||
<el-menu-item index="/admin/employees" @click="goTo('/admin/employees')">代理列表</el-menu-item>
|
||||
<el-menu-item index="/admin/employees/add" @click="goTo('/admin/employees/add')">添加代理</el-menu-item>
|
||||
<el-menu-item idnex="/admin/employee-type" @click="goTo('/admin/employee-type')">账户类型</el-menu-item>
|
||||
<el-menu-item index="/admin/employee-type" @click="goTo('/admin/employee-type')">账户类型</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 以下菜单 - 所有登录用户可见 -->
|
||||
|
||||
@ -96,7 +96,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { Loading, WarningFilled, Picture } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { getBookInfo } from '@/api/book'
|
||||
|
||||
interface BookPic {
|
||||
localPath?: string
|
||||
@ -169,9 +169,7 @@ async function fetchBookInfo(isbn: string) {
|
||||
bookData.value = null
|
||||
|
||||
try {
|
||||
const payload = await request.get('/getBookInfo', {
|
||||
params: { isbn }
|
||||
})
|
||||
const payload = await getBookInfo(isbn)
|
||||
const data = payload?.data
|
||||
|
||||
if (!data) {
|
||||
|
||||
@ -108,6 +108,9 @@ import type { GoodsInfo } from './goodsInfo.vue'
|
||||
import SuitBookDialog from './suitBookDialog.vue'
|
||||
import OcrResultDialog from './OcrResultDialog.vue'
|
||||
import { ocrImage } from '@/api/product'
|
||||
import { generateBarcode } from '@/api/barcode'
|
||||
import { getBookInfo, syncBook } from '@/api/book'
|
||||
import { queryGoodsPrice } from '@/api/config'
|
||||
import { getAdminUserInfo } from '@/utils/auth'
|
||||
import axios from 'axios'
|
||||
|
||||
@ -244,28 +247,24 @@ async function queryGoodsApi(isbn: string, productId: number, quality: string):
|
||||
const ip = localStorage.getItem('test_ip') || '127.0.0.1'
|
||||
const port = localStorage.getItem('test_port') || '8080'
|
||||
|
||||
const formData = new URLSearchParams()
|
||||
formData.append('isbn', isbn)
|
||||
formData.append('out_id', productId.toString())
|
||||
formData.append('quality', quality.toString())
|
||||
formData.append('query_index', queryIndex.toString())
|
||||
formData.append('user_id', userId.toString())
|
||||
formData.append('placeholder_down_price', localStorage.getItem('placeholder_down_price') || '0.01')
|
||||
formData.append('min_shipping_fee', localStorage.getItem('min_shipping_fee') || '5.00')
|
||||
formData.append('min_price', localStorage.getItem('min_price') || '1.00')
|
||||
|
||||
console.log('[goods/query] 发送请求 - 直连:', { ip, port, isbn, productId, quality })
|
||||
|
||||
try {
|
||||
// 直接请求核价器,避免 CORS 需服务端支持
|
||||
const response = await axios.post(`http://${ip}:${port}/api/goods/query`, formData.toString(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
timeout: 10000
|
||||
const response = await queryGoodsPrice({
|
||||
ip,
|
||||
port,
|
||||
isbn,
|
||||
outId: productId,
|
||||
quality,
|
||||
queryIndex,
|
||||
userId,
|
||||
placeholderDownPrice: localStorage.getItem('placeholder_down_price') || '0.01',
|
||||
minShippingFee: localStorage.getItem('min_shipping_fee') || '5.00',
|
||||
minPrice: localStorage.getItem('min_price') || '1.00'
|
||||
})
|
||||
|
||||
console.log('[goods/query] 响应:', response.data)
|
||||
console.log('[goods/query] 响应:', response)
|
||||
} catch (err) {
|
||||
// console.error('[goods/query] 请求失败:', err instanceof Error ? err.message : String(err))
|
||||
// 网络错误不中断流程
|
||||
@ -1778,12 +1777,8 @@ async function generateWaveBarcode() {
|
||||
|
||||
barcodeLoading.value = true
|
||||
try {
|
||||
const request = (await import('@/utils/request')).default
|
||||
const res = await request.post('/barcode/generate', {
|
||||
content: waveNo.value
|
||||
})
|
||||
const res = await generateBarcode(waveNo.value)
|
||||
|
||||
// res 已经被拦截器解包为 response.data,类型断言绕过 AxiosResponse 类型
|
||||
const resData = res as any
|
||||
const imageBase64 = resData.data?.image_base64
|
||||
if (resData.code === 200 && imageBase64) {
|
||||
@ -2303,10 +2298,7 @@ async function loadBookInfo(isbn: string) {
|
||||
lastScannedIsbn.value = isbn
|
||||
|
||||
try {
|
||||
const request = (await import('@/utils/request')).default
|
||||
const payload = await request.get('/getBookInfo', {
|
||||
params: { isbn }
|
||||
})
|
||||
const payload = await getBookInfo(isbn)
|
||||
const data = payload?.data
|
||||
if (!data) {
|
||||
bookInfo.value = null
|
||||
@ -2878,25 +2870,7 @@ async function callSyncBookApi(params: {
|
||||
type?: string
|
||||
}) {
|
||||
try {
|
||||
const request = (await import('@/utils/request')).default
|
||||
// 传普通对象,拦截器会自动做:签名 → objectToFormData → 移除 Content-Type
|
||||
const body: Record<string, any> = {}
|
||||
if (params.book_name) body['book_name'] = params.book_name
|
||||
if (params.author) body['author'] = params.author
|
||||
if (params.publisher) body['publisher'] = params.publisher
|
||||
if (params.publication_time) body['publication_time'] = params.publication_time
|
||||
if (params.binding_layout) body['binding_layout'] = params.binding_layout
|
||||
if (params.fix_price !== undefined) body['fix_price'] = params.fix_price
|
||||
if (params.isbn) body['isbn'] = params.isbn
|
||||
if (params.page_count) body['page_count'] = params.page_count
|
||||
if (params.word_count) body['word_count'] = params.word_count
|
||||
if (params.book_format) body['book_format'] = params.book_format
|
||||
if (params.fid !== undefined) body['fid'] = params.fid
|
||||
if (params.f_isbn) body['f_isbn'] = params.f_isbn
|
||||
if (params.f_book_name) body['f_book_name'] = params.f_book_name
|
||||
if (params.live_image) body['live_image[0]'] = params.live_image
|
||||
if (params.type) body['type'] = params.type
|
||||
const res = await request.post('/syncBook', body)
|
||||
const res = await syncBook(params)
|
||||
console.log('[syncBook] 同步成功:', res)
|
||||
return res
|
||||
} catch (err) {
|
||||
|
||||
@ -13,8 +13,8 @@ const USE_MOCK = false // 设置为true使用模拟数据
|
||||
|
||||
// 创建axios实例
|
||||
const request = axios.create({
|
||||
// baseURL: import.meta.env.DEV ? '/api' : (import.meta.env.VITE_API_BASE || 'http://192.168.101.213:9090/api'),
|
||||
baseURL: import.meta.env.DEV ? '/api' : (import.meta.env.VITE_API_BASE || 'https://psi.api.buzhiyushu.cn/api'),
|
||||
baseURL: import.meta.env.DEV ? '/api' : (import.meta.env.VITE_API_BASE || 'http://192.168.101.213:9090/api'),
|
||||
// baseURL: import.meta.env.DEV ? '/api' : (import.meta.env.VITE_API_BASE || 'https://psi.api.buzhiyushu.cn/api'),
|
||||
timeout: 10000,
|
||||
// 用 JSONbig 替代默认 JSON.parse,保留大整数精度
|
||||
transformResponse: [
|
||||
|
||||
@ -176,8 +176,8 @@ const loadLatestProducts = async () => {
|
||||
productsLoading.value = true
|
||||
try {
|
||||
// 当日的起始时间
|
||||
startOfDay = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000)
|
||||
endOfDay = Math.floor(new Date().setHours(23, 59, 59, 999) / 1000)
|
||||
const startOfDay = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000)
|
||||
const endOfDay = Math.floor(new Date().setHours(23, 59, 59, 999) / 1000)
|
||||
console.log('查询最新商品,时间范围:', new Date(startOfDay * 1000), ' - ', new Date(endOfDay * 1000))
|
||||
const data = await fetchLatestProducts(20, startOfDay, endOfDay)
|
||||
latestProducts.value = data
|
||||
|
||||
@ -129,7 +129,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User, Lock, Back, DocumentCopy } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { fetchEmployeeList, addEmployee } from '@/api/employee'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
@ -200,7 +200,7 @@ const fetchNextEmployeeId = async () => {
|
||||
try {
|
||||
// 这里可以调用接口获取下一个工号
|
||||
// 如果没有接口,使用模拟数据
|
||||
const res = await request.get('/admin/employee/list', { params: { page: 1, page_size: 1 } })
|
||||
const res = await fetchEmployeeList({ page: 1, page_size: 1 })
|
||||
if (res.code === 200 && res.data.list.length > 0) {
|
||||
const lastId = res.data.list[0].employee_id
|
||||
const seq = parseInt(lastId) + 1
|
||||
@ -243,7 +243,7 @@ const submitForm = async () => {
|
||||
if (currentInfo?.about_id) {
|
||||
params.about_id = currentInfo.about_id
|
||||
}
|
||||
const res = await request.post('/admin/employee/add', params)
|
||||
const res = await addEmployee(params)
|
||||
|
||||
if (res.code === 200) {
|
||||
resultData.value = res.data
|
||||
|
||||
@ -199,7 +199,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search, Coin, DataLine, Switch, Check } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { fetchEmployeeList, topupEmployee, deductEmployee, toggleEmployeeStatus } from '@/api/employee'
|
||||
|
||||
const router = useRouter() // 导入 router
|
||||
const loading = ref(false)
|
||||
@ -267,7 +267,7 @@ const formatDate = (timestamp) => {
|
||||
}
|
||||
|
||||
// 获取代理列表
|
||||
const fetchEmployeeList = async () => {
|
||||
const loadEmployeeList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
@ -281,7 +281,7 @@ const fetchEmployeeList = async () => {
|
||||
params.keyword = queryParams.keyword
|
||||
}
|
||||
|
||||
const res = await request.get('/admin/employee/list', { params })
|
||||
const res = await fetchEmployeeList(params)
|
||||
if (res.code === 200) {
|
||||
employeeList.value = res.data.list
|
||||
total.value = res.data.total
|
||||
@ -296,7 +296,7 @@ const fetchEmployeeList = async () => {
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
queryParams.page = 1
|
||||
fetchEmployeeList()
|
||||
loadEmployeeList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
@ -304,19 +304,19 @@ const resetSearch = () => {
|
||||
queryParams.status = ''
|
||||
queryParams.keyword = ''
|
||||
queryParams.page = 1
|
||||
fetchEmployeeList()
|
||||
loadEmployeeList()
|
||||
}
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size) => {
|
||||
queryParams.page_size = size
|
||||
fetchEmployeeList()
|
||||
loadEmployeeList()
|
||||
}
|
||||
|
||||
// 页码变化
|
||||
const handleCurrentChange = (page) => {
|
||||
queryParams.page = page
|
||||
fetchEmployeeList()
|
||||
loadEmployeeList()
|
||||
}
|
||||
|
||||
// 显示充值弹窗
|
||||
@ -331,14 +331,14 @@ const showTopUp = (row) => {
|
||||
const submitTopUp = async () => {
|
||||
topUpLoading.value = true
|
||||
try {
|
||||
const res = await request.post(`/admin/employee/topup/${currentEmployee.value.employee_id}`, {
|
||||
const res = await topupEmployee(currentEmployee.value.employee_id, {
|
||||
amount: topUpForm.amount,
|
||||
remark: topUpForm.remark
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('充值成功')
|
||||
topUpVisible.value = false
|
||||
fetchEmployeeList() // 刷新列表
|
||||
loadEmployeeList() // 刷新列表
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('充值失败:', error)
|
||||
@ -358,14 +358,14 @@ const showDeduct = (row) => {
|
||||
const submitDeduct = async () => {
|
||||
deductLoading.value = true
|
||||
try {
|
||||
const res = await request.post(`/admin/employee/deduct/${currentEmployee.value.employee_id}`, {
|
||||
const res = await deductEmployee(currentEmployee.value.employee_id, {
|
||||
amount: deductForm.amount,
|
||||
remark: deductForm.remark
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('扣减成功')
|
||||
deductVisible.value = false
|
||||
fetchEmployeeList() // 刷新列表
|
||||
loadEmployeeList() // 刷新列表
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error('扣减失败:', error)
|
||||
@ -383,10 +383,10 @@ const toggleStatus = async (row, action) => {
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await request.post(`/admin/employee/${action}/${row.employee_id}`)
|
||||
const res = await toggleEmployeeStatus(action, row.employee_id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(`${actionText}成功`)
|
||||
fetchEmployeeList() // 刷新列表
|
||||
loadEmployeeList() // 刷新列表
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
@ -408,7 +408,7 @@ const viewAccess = (row) => {
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchEmployeeList()
|
||||
loadEmployeeList()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -289,6 +289,7 @@ import {
|
||||
fetchTokenList,
|
||||
saveNewPrice,
|
||||
fetchConfig,
|
||||
deleteToken,
|
||||
} from '@/api/config'
|
||||
|
||||
const STORAGE_KEY_FILE_DIR = 'file_dir'
|
||||
@ -444,9 +445,7 @@ export default {
|
||||
type: 'warning'
|
||||
})
|
||||
// 调用删除接口
|
||||
await axios.get(`http://${ip.value}:${port.value}/api/token/delete`, {
|
||||
params: { id: entry.id }
|
||||
})
|
||||
await deleteToken(ip.value, port.value, entry.id!)
|
||||
// 同时清除 username 对应的 localStorage key
|
||||
localStorage.removeItem(entry.username)
|
||||
const list = loadSavedAccounts().filter(a => a.account !== entry.account)
|
||||
|
||||
@ -72,7 +72,7 @@
|
||||
User,
|
||||
Lock
|
||||
} from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { login } from '@/api/login'
|
||||
import {
|
||||
setAdminToken,
|
||||
setAdminUserInfo
|
||||
@ -132,8 +132,7 @@
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const type = activeTab.value === 'admin' ? '255' : '128'
|
||||
const res = await request.post(`/login/${type}`, { ...formData, type: 1 })
|
||||
const res = await login(activeTab.value, { ...formData })
|
||||
|
||||
if (res.code === 200) {
|
||||
setAdminToken(res.data.token)
|
||||
|
||||
@ -217,7 +217,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import { fetchProductLogList, deleteProductLog } from '@/api/reviewIllegalBook'
|
||||
import SubmIllegalBook from '@/components/dialog/submIllegalBook/SubmIllegalBook.vue'
|
||||
|
||||
// 搜索表单
|
||||
@ -334,22 +334,14 @@ function handlePageChange() {
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await request.get('/product_log/list', {
|
||||
params: {
|
||||
barcode: searchForm.isbn || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize
|
||||
}
|
||||
const { list, total } = await fetchProductLogList({
|
||||
barcode: searchForm.isbn || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize
|
||||
})
|
||||
const data = res?.data
|
||||
if (data) {
|
||||
tableData.value = Array.isArray(data.list) ? data.list : Array.isArray(data) ? data : []
|
||||
pagination.total = data.total || 0
|
||||
} else {
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
tableData.value = list
|
||||
pagination.total = total
|
||||
} catch (err) {
|
||||
ElMessage.error('获取列表失败')
|
||||
} finally {
|
||||
@ -369,11 +361,7 @@ async function handleReject(row: any) {
|
||||
await ElMessageBox.confirm('确认删除该条记录?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
const formData = new FormData()
|
||||
formData.append('id', String(row.id))
|
||||
await request.post('/product_log/delete', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
await deleteProductLog(row.id)
|
||||
ElMessage.success('已删除')
|
||||
fetchData()
|
||||
} catch {
|
||||
|
||||
@ -221,9 +221,8 @@ import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh, View, Loading, CopyDocument } from '@element-plus/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import axios from 'axios'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import { fetchShippingOrderList, fetchShippingOrderDetail, updateShippingOrderLogistics } from '@/api/shipping-order'
|
||||
import { fetchShippingOrderList, fetchShippingOrderDetail, updateShippingOrderLogistics, fetchFastMailList, createOrderBatch, createBmOrderDaYin, submitCompanyOrder } from '@/api/shipping-order'
|
||||
import { fetchWarehouseList } from '@/api/warehouse'
|
||||
import { createPrintTask } from '@/api/print'
|
||||
|
||||
@ -434,13 +433,11 @@ export default defineComponent({
|
||||
|
||||
try {
|
||||
// 第一步:获取快递账号列表
|
||||
const res = await axios.get('https://api.buzhiyushu.cn/zhishu/fastMail/listApi', {
|
||||
params: { shopId: salesPersonId }
|
||||
})
|
||||
console.log('fastMail/listApi 响应:', res.data)
|
||||
const res = await fetchFastMailList(salesPersonId)
|
||||
console.log('fastMail/listApi 响应:', res)
|
||||
|
||||
// 第二步:提取 fastMailType=1 的成员,调用创建面单接口
|
||||
const data = res.data?.data
|
||||
const data = res?.data
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
ElMessage.error('未获取到快递账号配置')
|
||||
isProcessing.value = false
|
||||
@ -458,33 +455,31 @@ export default defineComponent({
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('type', fastMail.type)
|
||||
formData.append('partnerId', fastMail.partnerId)
|
||||
formData.append('secret', fastMail.secret)
|
||||
formData.append('orderSn', currentItem.association_order_no)
|
||||
formData.append('contact', currentItem.warehouse_contact_person)
|
||||
formData.append('phoneNumber', currentItem.warehouse_contact_phone)
|
||||
formData.append('province', currentItem.warehouse_province)
|
||||
formData.append('city', currentItem.warehouse_city)
|
||||
formData.append('area', currentItem.warehouse_district)
|
||||
formData.append('town', currentItem.warehouse_address)
|
||||
const createRes = await axios.post('https://print.buzhiyushu.cn/api/print/createOrderBatch', formData)
|
||||
console.log('createOrderBatch 响应:', createRes.data)
|
||||
const createRes = await createOrderBatch({
|
||||
type: fastMail.type,
|
||||
partnerId: fastMail.partnerId,
|
||||
secret: fastMail.secret,
|
||||
orderSn: currentItem.association_order_no,
|
||||
contact: currentItem.warehouse_contact_person,
|
||||
phoneNumber: currentItem.warehouse_contact_phone,
|
||||
province: currentItem.warehouse_province,
|
||||
city: currentItem.warehouse_city,
|
||||
area: currentItem.warehouse_district,
|
||||
town: currentItem.warehouse_address
|
||||
})
|
||||
console.log('createOrderBatch 响应:', createRes)
|
||||
|
||||
// 第三步:获取打印PDF信息
|
||||
const createData = await axios.get('https://print.buzhiyushu.cn/api/print/createBmOrderDaYin', {
|
||||
params: {
|
||||
mailno: createRes.data.data[0].mail_no || createRes.data.data[0].mailno,
|
||||
partnerId: fastMail.partnerId,
|
||||
secret: fastMail.secret
|
||||
}
|
||||
const createData = await createBmOrderDaYin({
|
||||
mailno: createRes.data[0].mail_no || createRes.data[0].mailno,
|
||||
partnerId: fastMail.partnerId,
|
||||
secret: fastMail.secret
|
||||
})
|
||||
console.log('createBmOrderDaYin 响应:', createData.data)
|
||||
console.log('createBmOrderDaYin 响应:', createData)
|
||||
|
||||
const printData = createRes.data.data[0]
|
||||
printData.pdf_info = createData.data.pdfInfo
|
||||
printData.itemList = createRes.data.erpGoodsOrderList[0].itemList;
|
||||
const printData = createRes.data[0]
|
||||
printData.pdf_info = createData.pdfInfo
|
||||
printData.itemList = createRes.erpGoodsOrderList[0].itemList;
|
||||
console.log('createBmOrderDaYin 响应:', JSON.stringify(printData))
|
||||
|
||||
// 调用createPrintTask接口创建打印任务并打印(失败会阻断后续回填操作)
|
||||
@ -500,22 +495,20 @@ export default defineComponent({
|
||||
shipping_order_id: currentItem.shipping_order_id,
|
||||
sales_order_item_id: currentItem.sales_order_item_id,
|
||||
logistics_company: fastMail.type,
|
||||
logistics_no: createRes.data.data[0].mail_no || createRes.data.data[0].mailno
|
||||
logistics_no: createRes.data[0].mail_no || createRes.data[0].mailno
|
||||
})
|
||||
}
|
||||
|
||||
console.log('正在回填快递单号并更新状态...')
|
||||
const submitCompany = await axios.post('https://api.buzhiyushu.cn/zhishu/orderExternalGoods/submitCompanyOrder', {
|
||||
const submitCompany = await submitCompanyOrder({
|
||||
code: fastMail.type,
|
||||
orderNo: createRes.data.data[0].mail_no || createRes.data.data[0].mailno,
|
||||
erpOrderId: createRes.data.erpGoodsOrderList[0].id.toString()
|
||||
}, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
orderNo: createRes.data[0].mail_no || createRes.data[0].mailno,
|
||||
erpOrderId: createRes.erpGoodsOrderList[0].id.toString()
|
||||
});
|
||||
console.log('回填快递单号 响应:', submitCompany.data)
|
||||
console.log('回填快递单号 响应:', submitCompany)
|
||||
|
||||
// 回填快递单号到当前行
|
||||
scanList.value[currentScanIndex.value].logistics_no = createRes.data.data[0].mail_no || createRes.data.data[0].mailno
|
||||
scanList.value[currentScanIndex.value].logistics_no = createRes.data[0].mail_no || createRes.data[0].mailno
|
||||
} catch (err) {
|
||||
console.error('发货处理失败:', err)
|
||||
ElMessage.error('发货处理失败,请重试')
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="wave-page">
|
||||
|
||||
<Car ref="carRef" :disabled="isPageLocked || hasActiveWave" :on-query="handleIsbnQuery" />
|
||||
<Car ref="carRef" :disabled="isPageLocked" :on-query="handleIsbnQuery" />
|
||||
|
||||
<!-- 页面锁定遮罩层 -->
|
||||
<div class="page-lock-overlay" v-if="isPageLocked"></div>
|
||||
@ -175,12 +175,6 @@ const selectedCarCapacity = computed(() => {
|
||||
return car.selectedCarCapacity
|
||||
})
|
||||
|
||||
// 是否存在活跃波次(由 camera.vue 暴露,用于控制小车组件禁用状态)
|
||||
const hasActiveWave = computed(() => {
|
||||
const cam = cameraRef.value as { hasActiveWave?: boolean } | null
|
||||
return cam?.hasActiveWave ?? false
|
||||
})
|
||||
|
||||
// 当前预览的 goodsInfo(展示区显示的数据)
|
||||
const currentGoods = ref<GoodsInfoType | null>(null)
|
||||
|
||||
|
||||
Binary file not shown.
@ -174,8 +174,8 @@ module.exports = defineConfig({
|
||||
historyApiFallback: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
// target: 'http://192.168.101.213:9090',
|
||||
target: 'https://psi.api.buzhiyushu.cn',
|
||||
target: 'http://192.168.101.213:9090',
|
||||
// target: 'https://psi.api.buzhiyushu.cn',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/api/print': {
|
||||
|
||||
454
使用文档.md
Normal file
454
使用文档.md
Normal file
@ -0,0 +1,454 @@
|
||||
# 图书进销存系统 — 使用文档
|
||||
|
||||
> **系统名称**:Cards(图书进销存管理后台)
|
||||
> **适用对象**:系统管理员、运营人员、仓库人员
|
||||
> **最后更新**:2026-06-03
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [系统概述](#1-系统概述)
|
||||
2. [登录与首页](#2-登录与首页)
|
||||
3. [导航与布局](#3-导航与布局)
|
||||
4. [代理管理(管理员)](#4-代理管理管理员)
|
||||
5. [仓库与基础数据管理](#5-仓库与基础数据管理)
|
||||
6. [波次管理(核心工作流)](#6-波次管理核心工作流)
|
||||
7. [采购与销售](#7-采购与销售)
|
||||
8. [出库与发货](#8-出库与发货)
|
||||
9. [库存管理](#9-库存管理)
|
||||
10. [商品与店铺管理](#10-商品与店铺管理)
|
||||
11. [系统配置](#11-系统配置)
|
||||
12. [常见操作指引](#12-常见操作指引)
|
||||
13. [附:菜单速查表](#13-附菜单速查表)
|
||||
|
||||
---
|
||||
|
||||
## 1. 系统概述
|
||||
|
||||
图书进销存系统是一个面向图书行业的全流程管理平台,覆盖从图书**采购入库 → 库存管理 → 商品发布 → 销售出库 → 发货打单 → 盘库盘点**的完整业务链条。
|
||||
|
||||
系统主要服务两类角色:
|
||||
|
||||
| 角色 | 可访问的功能 |
|
||||
|------|-------------|
|
||||
| **管理员**(role=255) | 全部功能,包括代理管理、异常书目审核、核价器配置等 |
|
||||
| **普通用户/代理**(role=128) | 仪表盘、波次管理、仓库管理、采购销售、库存等基础业务 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 登录与首页
|
||||
|
||||
### 2.1 登录
|
||||
|
||||
1. 在浏览器中访问系统地址(开发环境:`http://localhost:5174`)
|
||||
2. 输入**用户名**和**密码**
|
||||
3. 选择登录身份:
|
||||
- **管理员登录** — 管理员账号,拥有全部权限
|
||||
- **代理登录** — 代理账号,权限受限
|
||||
4. 点击「登录」按钮进入系统
|
||||
|
||||
> **提示**:登录信息保存在浏览器中,关闭页面后重新打开无需重复登录。如忘记密码,请联系系统管理员重置。
|
||||
|
||||
### 2.2 首页(仪表盘)
|
||||
|
||||
登录后默认进入**仪表盘**页面,展示今日运营概况:
|
||||
|
||||
| 统计卡片 | 说明 |
|
||||
|---------|------|
|
||||
| **今日入库** | 今日采购入库数量(件) |
|
||||
| **今日出库** | 今日出库单数量 |
|
||||
| **今日销售** | 今日销售订单数量 |
|
||||
|
||||
页面下方两个列表:
|
||||
|
||||
- **今日最新商品** — 今日新录入的商品列表(名称、条码、价格、创建时间)
|
||||
- **今日员工统计** — 各员工今日的入库/出库工作量统计
|
||||
|
||||
---
|
||||
|
||||
## 3. 导航与布局
|
||||
|
||||
### 3.1 界面结构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 进销存系统 [折叠按钮] 用户名 ▼ 积分 │ ← 顶部栏
|
||||
├──────────┬──────────────────────────────────┤
|
||||
│ │ │
|
||||
│ 仪表盘 │ │
|
||||
│ 代理管理 │ 主内容区域 │
|
||||
│ ├ 代理列表│ (router-view) │
|
||||
│ ├ 添加代理│ │
|
||||
│ └ 账户类型│ │
|
||||
│ 供应商管理│ │
|
||||
│ 仓库管理 │ │
|
||||
│ ├ 仓库列表│ │
|
||||
│ ├ 小车管理│ │
|
||||
│ ├ 店铺管理│ │
|
||||
│ ├ 打印机 │ │
|
||||
│ └ PDA管理│ │
|
||||
│ 波次管理 │ │
|
||||
│ 采购/商品 │ │
|
||||
│ 销售/出库 │ │
|
||||
│ 库存/盘库 │ │
|
||||
│ ... │ │
|
||||
└──────────┴──────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 操作入口
|
||||
|
||||
- **左侧菜单** — 按功能模块分组,点击菜单项进入对应页面
|
||||
- **折叠按钮** — 顶部栏左侧汉堡图标,可收起/展开左侧菜单(窄屏时有用)
|
||||
- **用户菜单** — 顶部栏右侧显示当前用户名,点击可退出登录
|
||||
- **积分显示** — 代理用户的当前积分显示在顶部栏右侧
|
||||
|
||||
### 3.3 嵌入模式
|
||||
|
||||
系统支持通过 URL 参数嵌入到其他系统:
|
||||
|
||||
```
|
||||
http://host:port/path?token=xxx
|
||||
```
|
||||
|
||||
嵌入模式下会自动隐藏左侧导航栏和顶部栏,仅显示内容区域。
|
||||
|
||||
---
|
||||
|
||||
## 4. 代理管理(管理员)
|
||||
|
||||
代理管理仅**管理员**(role=255)可见。
|
||||
|
||||
### 4.1 代理列表
|
||||
|
||||
- 展示所有代理账号的列表
|
||||
- 支持搜索关键词(工号/姓名/账号)
|
||||
- 可对代理进行**充值**(增加积分)或**扣减**(减少积分)
|
||||
- 可**启用/禁用**代理账号
|
||||
|
||||
### 4.2 添加代理
|
||||
|
||||
表单字段:
|
||||
- **姓名** — 代理真实姓名
|
||||
- **密码** — 登录密码
|
||||
- **手机号** — 联系电话
|
||||
- **上级 ID** — 可选,指定上级代理
|
||||
|
||||
### 4.3 账户类型
|
||||
|
||||
管理员工账户类型配置,可设置不同类型的权限和有效期。
|
||||
|
||||
---
|
||||
|
||||
## 5. 仓库与基础数据管理
|
||||
|
||||
### 5.1 供应商管理
|
||||
|
||||
管理图书采购来源:
|
||||
- **供应商列表** — 查看所有供应商(编号、名称、联系方式、状态)
|
||||
- **新增/编辑/删除** — 维护供应商信息
|
||||
- **搜索** — 按供应商名称或编码搜索
|
||||
|
||||
### 5.2 仓库管理
|
||||
|
||||
管理仓储资源:
|
||||
- **仓库列表** — 查看所有仓库(编码、名称、类型、状态)
|
||||
- **新增/编辑/删除** — 维护仓库信息
|
||||
- **搜索** — 按仓库编码或名称搜索
|
||||
|
||||
### 5.3 库位管理
|
||||
|
||||
管理仓库内的存储位置:
|
||||
- **库位列表** — 查看所有库位(编码、类型、容量、状态)
|
||||
- **新增/编辑/删除** — 维护库位信息
|
||||
- **批量生成** — 按规则一次性生成多个库位(例如 A-01-01 到 A-10-10)
|
||||
- **批量更新** — 批量修改库位属性
|
||||
|
||||
### 5.4 小车管理
|
||||
|
||||
管理仓库作业用的小推车/搬运车:
|
||||
- **小车列表** — 查看所有小车(编号、名称)
|
||||
- **新增/编辑/删除** — 维护小车信息
|
||||
- **店铺关联** — 将小车与特定店铺关联,方便拣货作业
|
||||
|
||||
### 5.5 店铺管理
|
||||
|
||||
管理销售渠道:
|
||||
- **店铺列表** — 查看所有店铺(名称、类型、授权状态)
|
||||
- **新增/编辑/删除** — 维护店铺信息
|
||||
- **授权管理** — 更新店铺授权状态(已授权/未授权/已过期)
|
||||
- **支持平台**:拼多多、孔夫子旧书网、闲鱼等
|
||||
|
||||
### 5.6 打印机管理
|
||||
|
||||
管理系统中配置的打印机设备,用于打印快递面单等。
|
||||
|
||||
---
|
||||
|
||||
## 6. 波次管理(核心工作流)
|
||||
|
||||
波次管理是系统的核心操作模块,用于将图书信息录入到系统中并为后续的采购/出库做准备。
|
||||
|
||||
### 6.1 核价器配置
|
||||
|
||||
在使用波次功能前,需要先配置核价器(Pricer):
|
||||
- 输入核价器的 IP 地址和端口
|
||||
- **测试连接** — 验证能否连接到核价器
|
||||
- 配置价格参数(新书价格、占位降价、最低运费、最低价格等)
|
||||
- 管理核价器的登录 Token
|
||||
- **保存价格** — 将当前配置的价格参数保存到核价器
|
||||
|
||||
### 6.2 波次创建
|
||||
|
||||
波次创建是核心操作入口,使用**相机扫描 + 手动录入**结合的方式采集图书信息。
|
||||
|
||||
**操作步骤:**
|
||||
|
||||
1. **扫描条码** — 使用摄像头扫描图书条码(ISBN),自动查询书籍信息
|
||||
2. **OCR 识别** — 拍摄书籍封面,自动识别书名、作者、出版社(如有需要)
|
||||
3. **编辑信息** — 确认和修改书籍信息(书名、作者、出版社、定价、页数等)
|
||||
- 支持实拍图片上传
|
||||
- 支持套装书处理
|
||||
4. **生成条码** — 为商品生成条码图片
|
||||
5. **查询价格** — 从核价器获取商品的建议售价
|
||||
6. **提交保存** — 将商品信息保存到系统
|
||||
7. **创建波次** — 将一批商品归入同一波次,便于后续批量处理
|
||||
|
||||
### 6.3 波次任务列表
|
||||
|
||||
查看已创建的波次任务,支持按条件筛选和搜索。
|
||||
|
||||
---
|
||||
|
||||
## 7. 采购与销售
|
||||
|
||||
### 7.1 采购订单
|
||||
|
||||
管理图书采购全流程:
|
||||
- **采购单列表** — 查看所有采购单(单号、供应商、仓库、状态、总金额)
|
||||
- **筛选** — 按单号、状态、时间范围等条件筛选
|
||||
- **创建采购单** — 选择供应商、仓库,添加商品明细
|
||||
- **创建波次** — 采购单可创建波次进行入库操作
|
||||
- **查看详情** — 查看采购单的商品明细和状态
|
||||
|
||||
### 7.2 商品管理
|
||||
|
||||
管理所有商品信息:
|
||||
- **商品列表** — 查看所有商品(名称、条码、价格、图片、状态)
|
||||
- **搜索/筛选** — 按关键字、店铺、状态搜索
|
||||
- **新增/编辑/删除** — 维护商品信息
|
||||
- **实拍图片** — 上传和管理商品实拍图片(支持本地路径和拼多多 URL)
|
||||
- **商品发布** — 将商品发布到各个店铺(详见"发布记录")
|
||||
|
||||
### 7.3 销售订单
|
||||
|
||||
管理图书销售流程:
|
||||
- **销售单列表** — 查看所有销售订单(单号、客户、仓库、状态、金额)
|
||||
- **筛选** — 按单号、状态、客户等条件筛选
|
||||
- **创建销售单** — 选择客户、仓库,添加商品明细
|
||||
- **确认订单** — 确认销售订单后可创建出库单
|
||||
- **取消订单** — 可取消销售订单并填写原因
|
||||
|
||||
### 7.4 发布记录
|
||||
|
||||
查看商品发布到店铺的日志记录(当前为模拟数据):
|
||||
- 按店铺分组展示每个商品的发布状态
|
||||
- 状态包括:发布成功、发送到店铺失败、任务已创建未发送等
|
||||
- **重试发布** — 发布失败的商品可点击重试
|
||||
|
||||
---
|
||||
|
||||
## 8. 出库与发货
|
||||
|
||||
### 8.1 出库管理
|
||||
|
||||
- **出库单列表** — 查看所有出库单(单号、仓库、状态、日期)
|
||||
- **筛选** — 按单号、状态、仓库、日期范围筛选
|
||||
- **创建出库单** — 从销售订单创建或手动创建
|
||||
- **审核出库** — 审核通过后库存减少
|
||||
- **导出** — 导出出库单详情
|
||||
|
||||
### 8.2 发货单
|
||||
|
||||
- **发货单列表** — 查看所有发货单(单号、物流信息、状态)
|
||||
- **筛选** — 按单号、状态、仓库、客户等条件筛选
|
||||
- **生成发货单** — 从出库单生成
|
||||
- **填写物流信息** — 录入物流公司和快递单号
|
||||
- **打印面单** — 对接快递打印接口,批量打印快递面单
|
||||
- **回填快递单号** — 将快递单号回填到外部平台
|
||||
|
||||
---
|
||||
|
||||
## 9. 库存管理
|
||||
|
||||
### 9.1 库存查询
|
||||
|
||||
- **按商品汇总** — 按商品维度查看各仓库库存(总量/锁定量/可用量)
|
||||
- **按仓库明细** — 按仓库+库位+批次查看库存明细
|
||||
- **变动记录** — 查看库存变动日志(入库/出库/调整等)
|
||||
|
||||
### 9.2 盘库管理
|
||||
|
||||
用于定期盘点库存,确保系统库存与实际库存一致。
|
||||
|
||||
**操作流程:**
|
||||
|
||||
1. **创建盘库单**
|
||||
- 选择盘点类型:**全盘**(盘点仓库全部商品)或 **抽盘**(指定部分商品)
|
||||
- 选择仓库,填写备注
|
||||
2. **开始盘点** — 盘库单状态变为"盘点中"
|
||||
3. **录入盘点数据**
|
||||
- 逐条录入:输入实盘数量
|
||||
- 批量录入:一次性提交多条盘点结果
|
||||
4. **完成盘点** — 系统更新实际库存
|
||||
5. **调整库存** — 如果盘盈或盘亏,可手动调整库存(增加或减少)
|
||||
6. **取消/删除** — 待盘点状态可取消或删除盘库单
|
||||
|
||||
---
|
||||
|
||||
## 10. 商品与店铺管理
|
||||
|
||||
### 10.1 商品管理详情
|
||||
|
||||
- **商品列表** 支持按商品名称、条码搜索
|
||||
- 商品详情展示:名称、作者、出版社、定价(元)、条码、实拍图片等
|
||||
- 支持**套装书**标识
|
||||
|
||||
### 10.2 异常书目审核(管理员)
|
||||
|
||||
当系统检测到书目信息异常(如书名/作者/价格与标准数据不一致),会在该模块展示:
|
||||
|
||||
- **异常记录列表** — 展示所有被标记为异常的记录
|
||||
- **审核操作**:
|
||||
- **通过** — 确认新数据正确,更新到数据库
|
||||
- **驳回** — 拒绝异常标记,保留原数据
|
||||
- 可查看新旧数据对比(书名、作者、出版社、价格、页数等)
|
||||
|
||||
### 10.3 ISBN 悬停浮窗
|
||||
|
||||
在系统中任何显示 ISBN 的地方(如商品列表),将鼠标悬停在 ISBN 上会弹出浮窗,展示该书的详细信息:
|
||||
|
||||
- 封面图片
|
||||
- 书名、作者、出版社
|
||||
- 出版时间、装帧、定价
|
||||
- 页数、字数
|
||||
- 套装书标记
|
||||
|
||||
该功能有缓存机制,同一 ISBN 只查询一次。
|
||||
|
||||
---
|
||||
|
||||
## 11. 系统配置
|
||||
|
||||
### 11.1 核价器配置(管理员)
|
||||
|
||||
路径:波次管理 → 核价器配置
|
||||
|
||||
| 功能 | 操作说明 |
|
||||
|------|---------|
|
||||
| **连接设置** | 输入核价器的 IP 和端口,点击「测试连接」验证连通性 |
|
||||
| **价格配置** | 设置新书价格、占位降价金额、最低运费、最低书价 |
|
||||
| **价格保存** | 将配置的价格参数提交到核价器 |
|
||||
| **Token 管理** | 查看和管理核价器的登录 Token(新增/批量添加/删除) |
|
||||
| **孔夫子登录** | 在核价器中登录孔夫子旧书网账号,用于自动上架 |
|
||||
|
||||
### 11.2 物流模板管理
|
||||
|
||||
- **物流模板列表** — 查看所有物流模板(名称、运费等)
|
||||
- **新增/编辑/删除** — 维护物流模板
|
||||
- 关联行政区划数据(省/市/区)
|
||||
|
||||
### 11.3 分拣设置
|
||||
|
||||
分拣规则配置(当前为预留功能,菜单默认隐藏)。
|
||||
|
||||
### 11.4 店铺设置
|
||||
|
||||
店铺通用配置(当前为预留功能,菜单默认隐藏)。
|
||||
|
||||
---
|
||||
|
||||
## 12. 常见操作指引
|
||||
|
||||
### 12.1 快速录入一本书
|
||||
|
||||
```
|
||||
波次管理 → 进入波次页面
|
||||
→ 扫描 ISBN 条码(自动查询书籍信息)
|
||||
→ 确认书名、作者、出版社等信息
|
||||
→ 输入定价、页数等补充信息
|
||||
→ 可选:拍摄实拍图片 / OCR 识别封面
|
||||
→ 点击保存 → 商品自动创建
|
||||
```
|
||||
|
||||
### 12.2 处理一笔销售订单
|
||||
|
||||
```
|
||||
销售订单 → 创建销售单
|
||||
→ 选择客户、仓库 → 添加商品
|
||||
→ 确认并提交 → 列表中确认订单
|
||||
→ 进入出库管理 → 从销售单创建出库单
|
||||
→ 审核出库 → 库存减少
|
||||
→ 进入发货单 → 从出库单生成发货单
|
||||
→ 填写物流信息 → 打印面单
|
||||
```
|
||||
|
||||
### 12.3 进行月度盘点
|
||||
|
||||
```
|
||||
盘库管理 → 创建盘库单
|
||||
→ 选择仓库,盘点类型选"全盘"
|
||||
→ 确认创建
|
||||
→ 在列表中点击「开始盘点」
|
||||
→ 逐条输入实盘数量
|
||||
→ 全部完成后点击「完成盘点」
|
||||
→ 如有差异,使用「库存调整」功能修正
|
||||
```
|
||||
|
||||
### 12.4 查看商品库存
|
||||
|
||||
```
|
||||
库存记录 → 库存查询
|
||||
→ 选择商品 → 查看各仓库库存数量
|
||||
→ 切换「明细」视图 → 查看具体库位和批次
|
||||
→ 切换「变动记录」 → 查看该商品的出入库历史
|
||||
```
|
||||
|
||||
### 12.5 退出登录
|
||||
|
||||
点击顶部栏右侧的用户名 → 选择「退出登录」→ 确认即可。
|
||||
|
||||
---
|
||||
|
||||
## 13. 附:菜单速查表
|
||||
|
||||
| 菜单 | 子菜单 | 页面路径 | 功能 |
|
||||
|------|--------|---------|------|
|
||||
| (首页) | — | `/dashboard` | 今日运营数据概览 |
|
||||
| 代理管理 | 代理列表 | `/admin/employees` | 代理账号管理 |
|
||||
| 代理管理 | 添加代理 | `/admin/employees/add` | 新增代理 |
|
||||
| 代理管理 | 账户类型 | `/admin/employee-type` | 员工类型配置 |
|
||||
| 供应商管理 | 供应商列表 | `/supplier` | 供应商管理 |
|
||||
| 仓库管理 | 仓库列表 | `/warehouse` | 仓库管理 |
|
||||
| 仓库管理 | 小车管理 | `/car` | 小车管理 |
|
||||
| 仓库管理 | 店铺管理 | `/shop` | 店铺管理 |
|
||||
| 仓库管理 | 打印机管理 | `/printer-manager` | 打印机管理 |
|
||||
| 仓库管理 | PDA管理 | `/pdaManage` | PDA 管理 |
|
||||
| 波次管理 | 核价器配置 | `/testUrl` | 核价器配置 |
|
||||
| 波次管理 | 波次创建 | `/wave` | 相机扫码录书 |
|
||||
| 波次管理 | 波次任务列表 | `/wave-task` | 波次任务查看 |
|
||||
| 采购订单 | 采购单列表 | `/purchase-order` | 采购订单管理 |
|
||||
| 商品管理 | 商品列表 | `/product` | 商品管理 |
|
||||
| 商品管理 | 发布记录 | `/release-record` | 商品发布日志 |
|
||||
| 销售订单 | 销售单列表 | `/sales-order` | 销售订单管理 |
|
||||
| 出库管理 | 出库列表 | `/outbound` | 出库单管理 |
|
||||
| 发货单 | 发货单列表 | `/shipping-order` | 发货单管理 |
|
||||
| 库存记录 | 库存查询 | `/inventory` | 库存查询和变动记录 |
|
||||
| 盘库管理 | 盘库单列表 | `/stock-check` | 盘库盘点管理 |
|
||||
| — | 异常书目审核 | `/review-illegal-book` | 书目异常审核(管理员) |
|
||||
| — | 物流模板管理 | `/logistics` | 物流模板管理 |
|
||||
| — | 库位列表 | `/location` | 库位管理 |
|
||||
|
||||
---
|
||||
|
||||
*如在使用过程中遇到问题,请联系系统管理员或参考技术文档获取更多信息。*
|
||||
489
技术文档.md
Normal file
489
技术文档.md
Normal file
@ -0,0 +1,489 @@
|
||||
# 图书进销存系统 — 技术文档
|
||||
|
||||
> **项目名称**:Cards(图书进销存管理后台)
|
||||
> **版本**:1.0.0
|
||||
> **最后更新**:2026-06-03
|
||||
> **项目路径**:`D:\project\cards_web`
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [项目概述](#1-项目概述)
|
||||
2. [技术栈](#2-技术栈)
|
||||
3. [项目结构](#3-项目结构)
|
||||
4. [构建与部署](#4-构建与部署)
|
||||
5. [路由与权限](#5-路由与权限)
|
||||
6. [API 架构](#6-api-架构)
|
||||
7. [状态管理](#7-状态管理)
|
||||
8. [外部服务集成](#8-外部服务集成)
|
||||
9. [核心业务流程](#9-核心业务流程)
|
||||
10. [已知问题与维护](#10-已知问题与维护)
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
本项目是一个面向图书行业的**进销存管理后台系统**,覆盖图书采购入库、库存管理、商品发布、销售出库、发货打单、盘库盘点等全链路业务流程。系统支持多角色(管理员/代理)权限控制,并集成了核价器、快递打印、OCR 识别等外部服务。
|
||||
|
||||
### 核心业务范围
|
||||
|
||||
| 模块 | 说明 |
|
||||
|------|------|
|
||||
| **波次管理** | 相机扫码 → 条码识别 → 书籍信息编辑 → 波次创建/释放 |
|
||||
| **订单管理** | 采购订单 → 销售订单 → 出库单 → 发货单,全链路可追溯 |
|
||||
| **库存管理** | 库存查询(商品维度/仓库维度)、变动记录、盘库盘点 |
|
||||
| **商品管理** | 商品 CRUD、实拍图片管理、多店铺发布/重试 |
|
||||
| **店铺管理** | 多平台店铺(拼多多、孔夫子、闲鱼等)管理、授权状态 |
|
||||
| **系统管理** | 代理账号、员工类型、核价器配置、物流模板、打印机管理 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈
|
||||
|
||||
### 2.1 前端框架与依赖
|
||||
|
||||
| 依赖 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| `vue` | ^3.2.8 | 前端框架(Composition API + `<script setup>`) |
|
||||
| `vue-router` | ^4.1.6 | 前端路由(History 模式) |
|
||||
| `pinia` | ^2.1.7 | 状态管理 |
|
||||
| `element-plus` | ^2.13.7 | UI 组件库(中文 locale,small 尺寸) |
|
||||
| `@element-plus/icons-vue` | ^2.1.0 | 图标库 |
|
||||
| `axios` | ^1.7.9 | HTTP 客户端 |
|
||||
| `crypto-js` | ^4.2.0 | 签名算法(MD5/SHA256) |
|
||||
| `json-bigint` | ^1.0.0 | 大整数精度处理 |
|
||||
| `@zxing/library` | ^0.21.3 | 条形码/二维码扫描 |
|
||||
| `jsbarcode` | ^3.12.3 | 条码生成 |
|
||||
| `qrcode` | ^1.5.4 | QR 码生成 |
|
||||
|
||||
### 2.2 工程化
|
||||
|
||||
| 工具 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| `vite` | ^2.5.2 | 构建工具与开发服务器 |
|
||||
| `typescript` | ^5.5.4 | 类型检查(项目以 JS 为主,部分 .ts 文件) |
|
||||
| `sass` | ^1.99.0 | CSS 预处理器 |
|
||||
|
||||
### 2.3 环境变量(`.env`)
|
||||
|
||||
| 变量 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| `VITE_APP_API_KEY` | `psi` | API 签名密钥 |
|
||||
| `VITE_APP_CLIENT_ID` | `psi` | 客户端 ID |
|
||||
| `VITE_APP_API_SECRET` | `psi_api_sign_secret` | 签名密钥 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/ # API 层:所有后端接口调用集中于此
|
||||
│ ├── barcode.js # 条码生成
|
||||
│ ├── book.js # 书籍信息查询/同步
|
||||
│ ├── car.js # 小车管理
|
||||
│ ├── config.js # 核价器配置(直连外部 IP:Port)
|
||||
│ ├── dashboard.js # 仪表盘统计
|
||||
│ ├── district.js # 行政区划(省/市/区)及运费模板
|
||||
│ ├── employee.js # 员工/代理管理
|
||||
│ ├── employeeType.js # 员工类型管理
|
||||
│ ├── goodsInfo.js # 商品图像信息
|
||||
│ └── submIllegalBook.js # 提交异常书目
|
||||
│ ├── inventory.js # 库存记录
|
||||
│ ├── location.js # 库位管理
|
||||
│ ├── login.js # 登录
|
||||
│ ├── logistics.js # 物流模板
|
||||
│ ├── outbound.js # 出库管理
|
||||
│ ├── print.js # 打印服务(导出 Lodop 方法)
|
||||
│ ├── product.js # 商品管理
|
||||
│ ├── purchase-order.js # 采购订单
|
||||
│ ├── releaseRecord.js # 发布记录(当前为 Mock 数据)
|
||||
│ ├── reviewIllegalBook.js # 异常书目审核
|
||||
│ ├── sales-order.js # 销售订单
|
||||
│ ├── shipping-order.js # 发货单(含外部快递接口)
|
||||
│ ├── shop.js # 店铺管理(含外部 API + 内部 API)
|
||||
│ ├── stock-check.js # 盘库管理
|
||||
│ ├── supplier.js # 供应商管理
|
||||
│ ├── warehouse.js # 仓库管理
|
||||
│ └── wave-task.js # 波次任务
|
||||
│
|
||||
├── components/ # 公共组件
|
||||
│ ├── AdminLayout.vue # 主布局(侧边栏+顶部栏+内容区)
|
||||
│ ├── goodsPop/index.vue # ISBN 悬停浮窗(书品信息预览)
|
||||
│ ├── inventory/ # 库存子组件(byGoods, byLocation)
|
||||
│ ├── product/ # 商品子组件(byGoods, byLocation)
|
||||
│ ├── wave/ # 波次子组件(camera, car, bookInfo 等)
|
||||
│ └── wareHouse/ # 仓库子组件(WarehouseList, LocationManager)
|
||||
│
|
||||
├── views/ # 页面级组件
|
||||
│ ├── login/Login.vue # 登录页
|
||||
│ ├── admin/ # 管理员页面(Dashboard, EmployeeList, EmployeeAdd, employeeType)
|
||||
│ ├── wave/Wave.vue # 波次管理主页面
|
||||
│ ├── warehouse/ # 仓库列表
|
||||
│ ├── supplier/Supplier.vue # 供应商列表
|
||||
│ ├── product/Product.vue # 商品列表
|
||||
│ ├── purchase-order/ # 采购订单
|
||||
│ ├── sales-order/ # 销售订单
|
||||
│ ├── shipping-order/ # 发货单
|
||||
│ ├── outbound/Outbound.vue # 出库管理
|
||||
│ ├── inventory/Inventory.vue # 库存记录
|
||||
│ ├── stock-check/ # 盘库管理
|
||||
│ ├── shop/Shop.vue # 店铺管理
|
||||
│ ├── car/Car.vue # 小车管理
|
||||
│ ├── config/config.vue # 核价器配置
|
||||
│ ├── logistics/index.vue # 物流模板
|
||||
│ ├── releaseRecord/ # 发布记录
|
||||
│ ├── reviewIllegalBook/ # 异常书目审核
|
||||
│ ├── printer-manager/ # 打印机管理
|
||||
│ ├── shop-settings/ # 店铺设置
|
||||
│ ├── sorting-settings/ # 分拣设置
|
||||
│ ├── location/Location.vue # 库位列表
|
||||
│ ├── scanner/Scanner.vue # 扫码页
|
||||
│ ├── pdaManage/ # PDA 管理
|
||||
│ └── wave-task/WaveTask.vue # 波次任务列表
|
||||
│
|
||||
├── router/index.js # 路由配置(History 模式 + 路由守卫)
|
||||
├── store/ # 状态管理
|
||||
│ ├── index.js # 旧版 store(reactive 方式,逐步废弃)
|
||||
│ └── user.js # Pinia 用户 store
|
||||
├── utils/ # 工具函数
|
||||
│ ├── request.js # Axios 封装(拦截器、签名、Mock)
|
||||
│ ├── auth.js # Token/UserInfo 的 localStorage 读写
|
||||
│ ├── sign.js # 签名工具(MD5/SHA256,参数排序)
|
||||
│ ├── mock.js # Mock 数据
|
||||
│ ├── clipboard.js # 剪贴板工具
|
||||
│ ├── daYin/print.ts # Lodop 打印服务
|
||||
│ ├── daYinPdd/index.ts # 拼多多打印
|
||||
│ ├── pddUpload.ts # 拼多多图片上传
|
||||
│ └── ServiceManager.js # 服务管理器
|
||||
├── App.vue # 根组件
|
||||
└── main.js # 入口文件(注册 Element Plus / Pinia / Router)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 构建与部署
|
||||
|
||||
### 4.1 开发服务器
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# → http://localhost:5174 (host: 0.0.0.0)
|
||||
```
|
||||
|
||||
### 4.2 生产构建
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# → 输出到 dist/
|
||||
```
|
||||
|
||||
### 4.3 Vite 配置要点
|
||||
|
||||
在 `vite.config.js` 中:
|
||||
|
||||
| 配置项 | 值 | 说明 |
|
||||
|--------|-----|------|
|
||||
| **代理** | `/api` → `http://192.168.101.213:9090` | 主业务 API |
|
||||
| **代理** | `/api/print` → `http://192.168.101.127:8075` | 打印服务 |
|
||||
| **别名** | `@` → `./src` | 模块路径别名 |
|
||||
| **端口** | `5174` | 开发服务器端口 |
|
||||
| **自定义中间件** | `/kongfz-login` | 孔夫子登录代理(避免跨域) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 路由与权限
|
||||
|
||||
### 5.1 路由模式
|
||||
|
||||
`createWebHistory()` — History 模式,URL 格式为 `http://host:port/path`
|
||||
|
||||
### 5.2 路由表
|
||||
|
||||
| 路径 | 组件 | 标题 | 权限 |
|
||||
|------|------|------|------|
|
||||
| `/login` | Login.vue | 登录 | 无(已登录自动跳转) |
|
||||
| `/dashboard` | Dashboard.vue | 仪表盘 | 需认证 |
|
||||
| `/admin/employees` | EmployeeList.vue | 代理管理 | 管理员 |
|
||||
| `/admin/employees/add` | EmployeeAdd.vue | 添加代理 | 管理员 |
|
||||
| `/admin/employee-type` | employeeType.vue | 员工类型管理 | 管理员 |
|
||||
| `/wave` | Wave.vue | 波次管理 | 需认证 |
|
||||
| `/warehouse` | WareHouse.vue | 仓库列表 | 需认证 |
|
||||
| `/supplier` | Supplier.vue | 供应商管理 | 需认证 |
|
||||
| `/product` | Product.vue | 商品管理 | 需认证 |
|
||||
| `/purchase-order` | PurchaseOrder.vue | 采购订单 | 需认证 |
|
||||
| `/sales-order` | SalesOrder.vue | 销售订单 | 需认证 |
|
||||
| `/outbound` | Outbound.vue | 出库管理 | 需认证 |
|
||||
| `/shipping-order` | ShippingOrder.vue | 发货单 | 需认证 |
|
||||
| `/inventory` | Inventory.vue | 库存记录 | 需认证 |
|
||||
| `/stock-check` | StockCheck.vue | 盘库管理 | 需认证 |
|
||||
| `/release-record` | releaseRecord.vue | 发布记录 | 需认证 |
|
||||
| `/shop` | Shop.vue | 店铺管理 | 需认证 |
|
||||
| `/car` | Car.vue | 小车管理 | 需认证 |
|
||||
| `/location` | Location.vue | 库位列表 | 需认证 |
|
||||
| `/review-illegal-book` | ReviewIllegalBook.vue | 异常书目审核 | 管理员 |
|
||||
| `/testUrl` | config.vue | 核价器配置 | 管理员 |
|
||||
| `/printer-manager` | PrinterManager.vue | 打印机管理 | 需认证 |
|
||||
| `/logistics` | logistics/index.vue | 物流模板管理 | 需认证 |
|
||||
| ... | 其他 | 隐藏菜单路由 | 需认证 |
|
||||
|
||||
### 5.3 路由守卫逻辑
|
||||
|
||||
1. **URL 参数注入**:检测 `?token=xxx` 参数,自动写入 localStorage 并解析 JWT,用于跨系统嵌入
|
||||
2. **登录守卫**:未登录用户自动重定向到 `/login`
|
||||
3. **管理员守卫**:`requiresAdmin: true` 的路由仅 `role=255` 可访问,否则重定向到 `/dashboard`
|
||||
|
||||
### 5.4 角色权限
|
||||
|
||||
| role 值 | 身份 | 说明 |
|
||||
|---------|------|------|
|
||||
| `255` | 管理员 | 全部功能可用 |
|
||||
| `128` | 代理 | 仪表盘、波次、仓库、商品等基础功能 |
|
||||
|
||||
---
|
||||
|
||||
## 6. API 架构
|
||||
|
||||
### 6.1 网络拓扑
|
||||
|
||||
```
|
||||
浏览器 ──→ Vite Dev Server (:5174)
|
||||
├── /api/* ──→ Proxy ──→ http://192.168.101.213:9090 (主业务 API)
|
||||
├── /api/print/* ──→ Proxy ──→ http://192.168.101.127:8075 (打印服务)
|
||||
├── /kongfz-login ──→ Node.js 中间件 ──→ login.kongfz.com (孔夫子登录)
|
||||
└── axios 直连 ──→ 核价器 http://{ip}:{port} (用户配置)
|
||||
──→ https://api.buzhiyushu.cn (店铺列表/快递回填)
|
||||
──→ https://print.buzhiyushu.cn (快递面单打印)
|
||||
```
|
||||
|
||||
### 6.2 请求封装(`src/utils/request.js`)
|
||||
|
||||
核心流程:
|
||||
|
||||
```
|
||||
请求发起
|
||||
│
|
||||
├── 请求拦截器
|
||||
│ ├── 注入 Bearer Token(从 localStorage.admin_token 读取)
|
||||
│ ├── 生成签名参数(params/body → MD5 排序签名,可跳过)
|
||||
│ └── 对象自动转 FormData(POST/PUT/DELETE)
|
||||
│
|
||||
└── 响应拦截器
|
||||
├── code=200 → 返回 response.data
|
||||
├── code≠200 → 统一 ElMessage 错误提示
|
||||
├── HTTP 401 → 清除 token 并跳转登录页
|
||||
├── HTTP 403/404/500 → 对应错误提示
|
||||
└── 网络错误 → "检查网络连接"
|
||||
```
|
||||
|
||||
**签名机制**(`src/utils/sign.js`):
|
||||
- 参数平铺排序(支持嵌套对象和数组)
|
||||
- `app_secret + 参数字符串 + app_secret` 进行 MD5 签名
|
||||
- 自动注入 `app_key`、`client_id`、`timestamp`、`sign_method` 等系统参数
|
||||
- 通过请求头 `X-Need-Sign: false` 可跳过签名
|
||||
|
||||
**大整数精度**:
|
||||
- 使用 `json-bigint` 替换默认的 JSON.parse,防止后端返回的 19 位大整数丢失精度
|
||||
|
||||
### 6.3 API 模块总览
|
||||
|
||||
| 文件 | 接口前缀 | 风格 | 说明 |
|
||||
|------|---------|------|------|
|
||||
| `login.js` | `/login/{role}` | request | 管理员/代理登录 |
|
||||
| `dashboard.js` | `/dashboard/statist` | request | 仪表盘统计 |
|
||||
| `barcode.js` | `/barcode/generate` | request+FormData | 条码生成 |
|
||||
| `book.js` | `/getBookInfo`, `/syncBook` | request | 书籍查询/同步 |
|
||||
| `car.js` | `/car/*` | request | 小车 CRUD |
|
||||
| `config.js` | 外部 URL `http://{ip}:{port}` | axios 直连 | 核价器(价格查询/Token管理) |
|
||||
| `district.js` | `/district/*` | request | 省市区数据、运费模板 |
|
||||
| `employee.js` | `/admin/employee/*` | request | 代理增删改查、积分操作 |
|
||||
| `employeeType.js` | `/admin/user-type/*` | request | 员工类型管理 |
|
||||
| `goodsInfo.js` | `/product_log/save` | request+FormData | 提交书目异常 |
|
||||
| `inventory.js` | `/inventory/*` | request | 库存汇总/明细/变动记录 |
|
||||
| `location.js` | `/location/*` | request | 库位 CRUD、批量生成 |
|
||||
| `logistics.js` | `/logistics/*` | request | 物流模板 CRUD |
|
||||
| `outbound.js` | `/outbound-order/*` | request | 出库单 CRUD、审核、导出 |
|
||||
| `print.js` | — | 导出 ts 方法 | 打印服务(Lodop) |
|
||||
| `product.js` | `/product/*`, `/getSuitBook`, `/ocr` | request | 商品 CRUD、图片管理、OCR |
|
||||
| `purchase-order.js` | `/purchase-order/*`, `/wave/release` | request | 采购单 CRUD、波次释放 |
|
||||
| `releaseRecord.js` | — | **Mock** | 发布记录(纯前端模拟数据) |
|
||||
| `reviewIllegalBook.js` | `/product_log/*` | request | 异常书目查询/删除 |
|
||||
| `sales-order.js` | `/sales-order/*` | request | 销售订单 CRUD、确认、取消 |
|
||||
| `shipping-order.js` | `/shipping-order/*` | request | 发货单 CRUD + 外部直连 |
|
||||
| `shop.js` | `/shop/*` + 外部 URL | request+axios | 店铺 CRUD + 外部列表查询 |
|
||||
| `stock-check.js` | `/inventory/stock-check/*` | request | 盘库单 CRUD、盘点提交 |
|
||||
| `submIllegalBook.js` | `/product_log/audit` | request+FormData | 异常书目审核 |
|
||||
| `supplier.js` | `/supplier/*` | request | 供应商 CRUD |
|
||||
| `warehouse.js` | `/warehouse/*` | request | 仓库 CRUD |
|
||||
| `wave-task.js` | `/wave/*` | request | 波次任务/列表 |
|
||||
|
||||
### 6.4 列表接口标准化
|
||||
|
||||
大部分 API 模块实现了 `normalizeListResponse()` 工具函数,将后端可能返回的各种列表格式统一为 `{ list: Array, total: number }`:
|
||||
|
||||
```javascript
|
||||
const normalizeListResponse = (payload) => {
|
||||
const data = payload?.data
|
||||
if (!data) return { list: [], total: 0 }
|
||||
if (Array.isArray(data)) return { list: data, total: data.length }
|
||||
return {
|
||||
list: Array.isArray(data.list) ? data.list : [],
|
||||
total: typeof data.total === 'number' ? data.total : (data.list?.length || 0)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Mock 模式
|
||||
|
||||
`src/utils/request.js` 中 `USE_MOCK = false`。启用后支持模拟数据(商品、卡密、订单、代理等),用于离线开发调试。
|
||||
|
||||
---
|
||||
|
||||
## 7. 状态管理
|
||||
|
||||
### 7.1 Pinia Store(`src/store/user.js`)
|
||||
|
||||
| 状态/方法 | 类型 | 说明 |
|
||||
|-----------|------|------|
|
||||
| `adminUserInfo` | `ref` | 管理员用户信息(从 localStorage 初始化) |
|
||||
| `userInfo` | `computed` | adminUserInfo 的别名 |
|
||||
| `isAdmin` | `computed` | `role === 255` |
|
||||
| `points` | `computed` | 当前积分 |
|
||||
| `setUserInfoAction(info)` | 方法 | 更新用户信息(同步到 localStorage) |
|
||||
| `updatePoints(newPoints)` | 方法 | 更新积分 |
|
||||
| `clearAdminUserInfo()` | 方法 | 清除用户信息(登出) |
|
||||
|
||||
### 7.2 旧版 Store(`src/store/index.js`)
|
||||
|
||||
基于 Vue `reactive` 的简单 store,未使用 Pinia,逐步废弃中。
|
||||
|
||||
### 7.3 认证状态
|
||||
|
||||
认证信息存储于 `localStorage`,通过 `src/utils/auth.js` 统一管理:
|
||||
|
||||
| Key | 内容 | 用途 |
|
||||
|-----|------|------|
|
||||
| `admin_token` | JWT Token | 管理员 Bearer Token |
|
||||
| `admin_userInfo` | JSON 用户信息 | 管理员信息(含 role、about_id 等) |
|
||||
| `embed_mode` | `"true"` | 嵌入模式标志(隐藏导航栏) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 外部服务集成
|
||||
|
||||
### 8.1 核价器(Pricer)
|
||||
|
||||
- **方式**:通过 `axios` 直连(不经过项目主 API),连接用户配置的 IP:Port
|
||||
- **功能**:查询商品价格、设置价格、管理登录 Token
|
||||
- **文件**:`src/api/config.js`,全部为 axios 直连请求
|
||||
|
||||
### 8.2 快递打印
|
||||
|
||||
- **方式**:通过 `axios` 直连
|
||||
- **接口**:
|
||||
- `api.buzhiyushu.cn` — 快递账号列表、回填快递单号
|
||||
- `print.buzhiyushu.cn` — 创建快递面单、获取打印面单
|
||||
- **特点**:使用独立的 `axios` 实例(不经过 request 拦截器签名)
|
||||
- **文件**:`src/api/shipping-order.js`
|
||||
|
||||
### 8.3 店铺列表
|
||||
|
||||
- 内部接口(经过代理)和外部接口(`api.buzhiyushu.cn` 直连)两条路径
|
||||
- 外部接口使用独立 axios 实例 + JSONbig 处理大整数
|
||||
- **文件**:`src/api/shop.js`
|
||||
|
||||
### 8.4 孔夫子旧书网登录
|
||||
|
||||
- 通过 Vite 开发服务器自定义中间件 `/kongfz-login` 实现
|
||||
- 模拟浏览器登录流程:POST 表单 → 处理重定向 → 提取 PHPSESSID → 获取用户信息
|
||||
- 绕过浏览器跨域限制
|
||||
|
||||
### 8.5 OCR 识别
|
||||
|
||||
- 图片上传到 `/ocr` 接口进行文字识别
|
||||
- 跳过签名(`X-Need-Sign: false`),保持 FormData 中 Blob 数据完整性
|
||||
- **文件**:`src/api/product.js` 中的 `ocrImage()`
|
||||
|
||||
### 8.6 Lodop 打印
|
||||
|
||||
- `src/utils/daYin/print.ts` — Clodop/Lodop 打印服务封装
|
||||
- `src/utils/daYinPdd/index.ts` — 拼多多打印服务
|
||||
|
||||
---
|
||||
|
||||
## 9. 核心业务流程
|
||||
|
||||
### 9.1 波次管理流程
|
||||
|
||||
```
|
||||
相机扫码 (camera.vue)
|
||||
├── 扫描条码 → 查询书籍信息 (getBookInfo)
|
||||
├── OCR 识别 → 提取书名/作者/出版社 (ocrImage)
|
||||
├── 生成条码图片 (generateBarcode)
|
||||
├── 编辑书籍信息 → 同步到系统 (syncBook)
|
||||
└── 添加商品 → 创建波次 (createWaveOutbound)
|
||||
└── 释放波次 → 生成采购/出库任务 (releaseWave)
|
||||
```
|
||||
|
||||
### 9.2 采购到出库流程
|
||||
|
||||
```
|
||||
创建采购订单 (createPurchaseOrder)
|
||||
└── 创建波次 → 释放波次 → 生成波次任务
|
||||
└── 采购入库 → 库存增加
|
||||
└── 创建销售订单 (createSalesOrder)
|
||||
└── 确认销售订单 → 创建出库单 (createOutbound)
|
||||
└── 审核出库单 → 生成发货单 (createShippingOrder)
|
||||
└── 填写物流信息 → 回填快递单号 (submitCompanyOrder)
|
||||
```
|
||||
|
||||
### 9.3 盘库流程
|
||||
|
||||
```
|
||||
创建盘库单 (createStockCheck) — 全盘/抽盘
|
||||
└── 开始盘点 (startStockCheck) → 状态变为"盘点中"
|
||||
└── 逐条/批量提交盘点结果 (submitCheckItem / batchSubmitCheckItems)
|
||||
└── 完成盘点 (completeStockCheck) → 状态变为"已完成"
|
||||
```
|
||||
|
||||
### 9.4 店铺商品发布
|
||||
|
||||
```
|
||||
商品录入 → 选择店铺 → 发布商品
|
||||
└── 查看发布记录(当前为 Mock 数据)
|
||||
└── 失败重试 (retryRelease)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 已知问题与维护
|
||||
|
||||
### 10.1 遗留问题
|
||||
|
||||
| # | 问题 | 说明 | 建议 |
|
||||
|---|------|------|------|
|
||||
| 1 | Vite 2 版本较旧 | `^2.5.2` 有已知安全更新 | 建议升级到 Vite 4+ |
|
||||
| 2 | TypeScript 使用不一致 | `tsconfig.json` 存在但项目以 JS 为主 | 统一规范或移除 TS |
|
||||
| 3 | 发布记录为 Mock 数据 | `releaseRecord.js` 无后端接口 | 待后端接口对接 |
|
||||
| 4 | 旧版 Store 冗余 | `store/index.js` 与 Pinia 并存 | 可迁移清理 |
|
||||
| 5 | 备份文件残留 | `.bak` 后缀文件 | 确认后可删除 |
|
||||
|
||||
### 10.2 开发规范
|
||||
|
||||
- **API 层**:所有后端接口调用集中在 `src/api/`,视图层不直接调用 `request`
|
||||
- **列表接口**:统一使用 `normalizeListResponse()` 标准化返回格式
|
||||
- **接口文档**:优先用 JSDoc 注释描述参数和返回值
|
||||
- **命名**:API 函数使用 `fetchXxx`(查询)、`createXxx`(新增)、`updateXxx`(更新)、`deleteXxx`(删除)
|
||||
|
||||
### 10.3 添加新模块步骤
|
||||
|
||||
1. 在 `src/api/` 下新建 `<module>.js`,定义所有接口函数
|
||||
2. 在 `src/views/` 下新建页面目录及 `.vue` 组件
|
||||
3. 在 `src/router/index.js` 的 `children` 中添加路由
|
||||
4. 确保 `meta` 中正确设置 `requiresAuth` / `requiresAdmin`
|
||||
5. 在 `AdminLayout.vue` 中添加菜单项
|
||||
|
||||
---
|
||||
|
||||
*本文档随项目维护持续更新*
|
||||
Loading…
Reference in New Issue
Block a user