968 lines
34 KiB
Vue
968 lines
34 KiB
Vue
<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> |