Files
webs/power_leasing/src/views/account/myShops.vue
2025-10-31 14:09:58 +08:00

671 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="panel" >
<h2 class="panel-title">我的店铺</h2>
<div class="panel-body">
<!-- 店铺层级说明 -->
<el-card class="guide-card" shadow="never" style="margin-bottom: 16px;">
<div slot="header" class="guide-header">店铺层级说明</div>
<div class="guide-content">
<p class="hierarchy">层级结构店铺 商品 出售机器</p>
<ol class="guide-steps">
<li>
<b>店铺唯一</b>每个用户在平台<strong>仅能创建一个店铺</strong>创建成功后
请在本页点击 <b>钱包绑定</b>配置自己的收款地址支持不同链与币种
</li>
<li>
<b>商品</b>完成钱包绑定后即可在我的店铺页面 <b>创建商品</b>
商品可按 <b>币种</b> 进行分类管理创建的商品会在商城对买家展示
商品可理解为不同算法币种的机器集合分类
</li>
<li>
<b>出售机器</b>创建商品后请进入 <b>商品列表</b> 为该商品 <b>添加出售机器明细</b>
必须添加出售机器否则买家无法下单买家点击某个商品后会看到该商品下的机器明细并进行选购
</li>
</ol>
<div class="guide-note">提示建议先创建店铺 完成钱包绑定 创建商品 添加出售机器的顺序避免漏配导致无法收款或无法下单</div>
</div>
</el-card>
<el-card v-if="loaded && hasShop" class="shop-card" shadow="hover">
<div class="shop-row">
<div class="shop-cover">
<img :src="shop.image || defaultCover" alt="店铺封面" />
</div>
<div class="shop-info">
<div class="shop-title">
<span class="name">{{ shop.name || '未命名店铺' }}</span>
<el-tag size="small" :type="shopStateTagType">
{{ shopStateText }}
</el-tag>
</div>
<div class="desc">{{ shop.description || '这家店还没有描述~' }}</div>
<!-- <div class="meta">
<span>店铺ID{{ shop.id || '-' }}</span>
<span>可删除{{ shop.del ? '是' : '否' }}</span>
</div> -->
<div class="actions">
<el-button size="small" type="primary" @click="handleOpenEdit">修改店铺</el-button>
<el-button size="small" type="warning" @click="handleToggleShop">
{{ shop.state === 2 ? '开启店铺' : '关闭店铺' }}
</el-button>
<el-button size="small" type="danger" @click="handleDelete">删除店铺</el-button>
<el-button size="small" type="success" @click="handleAddProduct">新增商品</el-button>
<el-button size="small" type="success" @click="handleWalletBind">钱包绑定</el-button>
</div>
</div>
</div>
</el-card>
<!-- 店铺配置表格 -->
<el-card v-if="loaded && hasShop" class="shop-config-card" shadow="never" style="margin-top: 16px;">
<div slot="header" class="clearfix">
<span>已绑定钱包</span>
</div>
<el-table :data="shopConfigs" border style="width: 100%">
<el-table-column prop="chain" label="链" width="140" />
<el-table-column label="支付币种" >
<template slot-scope="scope">
<div class="coin-list">
<template v-if="Array.isArray(scope.row.children) && scope.row.children.length">
<el-tooltip
v-for="(c, idx) in scope.row.children"
:key="idx"
:content="String(c && c.payCoin ? c.payCoin : '').toUpperCase()"
placement="top"
>
<img
v-if="c && c.image"
class="coin-img"
:src="c.image"
:alt="(c.payCoin || '').toUpperCase()"
/>
</el-tooltip>
</template>
<template v-else>
{{ String(scope.row.payCoin || '').toUpperCase() }}
</template>
</div>
</template>
</el-table-column>
<!-- <el-table-column prop="payType" label="币种类型" width="120">
<template slot-scope="scope">{{ scope.row.payType === 1 ? '稳定币' : '虚拟币' }}</template>
</el-table-column> -->
<el-table-column prop="payAddress" label="收款钱包地址" show-overflow-tooltip />
<el-table-column label="操作" width="180" fixed="right">
<template slot-scope="scope">
<el-button type="text" @click="handleEditConfig(scope.row)">修改</el-button>
<el-divider direction="vertical"></el-divider>
<el-button type="text" style="color:#e74c3c" @click="handleDeleteConfig(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<div v-else-if="loaded && !hasShop" class="no-shop">
<el-empty description="暂无店铺">
<el-button type="primary" @click="handleGoNew">新建店铺</el-button>
</el-empty>
</div>
<el-empty v-else description="正在加载店铺信息..." />
<!-- 修改店铺弹窗 -->
<el-dialog title="修改店铺" :visible.sync="visibleEdit" width="520px">
<div class="row">
<label class="label">店铺名称</label>
<el-input v-model="editForm.name" placeholder="请输入店铺名称" :maxlength="30" show-word-limit />
</div>
<!-- <div class="row">
<label class="label">店铺封面</label>
<el-input v-model="editForm.image" placeholder="请输入图片地址" />
</div> -->
<div class="row">
<label class="label">店铺描述</label>
<el-input type="textarea" :rows="3" v-model="editForm.description" placeholder="请输入描述" :maxlength="300" show-word-limit />
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="visibleEdit=false">取消</el-button>
<el-button type="primary" @click="submitEdit">保存</el-button>
</span>
</el-dialog>
<!-- 修改钱包绑定配置弹窗参数保持与列表一致 -->
<el-dialog title="修改配置" :visible.sync="visibleConfigEdit" width="560px">
<div class="row">
<label class="label">支付链</label>
<el-input v-model="configForm.chainLabel" placeholder="-" disabled />
</div>
<div class="row">
<label class="label">支付币种</label>
<el-select
class="input"
size="middle"
v-model="configForm.payCoins"
multiple
collapse-tags
filterable
placeholder="请选择币种"
>
<el-option
v-for="item in editCoinOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="row">
<label class="label">已选择币种</label>
<div class="selected-coin-list">
<el-tag
v-for="c in selectedCoinLabels"
:key="c"
type="warning"
effect="light"
closable
@close="removeSelectedCoin(c)"
>{{ c }}</el-tag>
<span v-if="!selectedCoinLabels.length" style="color:#c0c4cc">未选择</span>
</div>
</div>
<div class="row">
<label class="label">钱包地址</label>
<el-input v-model="configForm.payAddress" placeholder="请输入钱包地址" />
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="visibleConfigEdit=false">取消</el-button>
<el-button type="primary" @click="submitConfigEdit">确认修改</el-button>
</span>
</el-dialog>
</div>
</div>
</template>
<script>
import { getMyShop, updateShop, deleteShop, queryShop, closeShop ,updateShopConfig,deleteShopConfig,getChainAndCoin} from '@/api/shops'
import { coinList } from '@/utils/coinList'
import { getShopConfig } from '@/api/wallet'
export default {
name: 'AccountMyShops',
data() {
return {
loaded: false,
defaultCover: 'https://dummyimage.com/120x120/eee/999.png&text=Shop',
shop: {
id: 0,
name: '',
image: '',
description: '',
del: true,
state: 0
},
visibleEdit: false,
editForm: { id: '', name: '', image: '', description: '' },
// 店铺配置列表
shopConfigs: [],
visibleConfigEdit: false,
configForm: { id: '', chainLabel: '', chainValue: '', payAddress: '', payCoins: [], payCoin: '' },
productOptions: [],
coinOptions: coinList || [],
editCoinOptionsApi: [],
// 支付链选项(可与后端接口对齐后替换为动态)
chainOptions: [
{ label: 'Tron (TRC20)', value: 'tron' },
{ label: 'Ethereum (ERC20)', value: 'ethereum' },
{ label: 'BSC (BEP20)', value: 'bsc' },
{ label: 'Nexa', value: 'nexa' },
],
shopLoading: false
}
},
computed: {
shopStateText() {
// 0 待审核 1 审核通过(店铺开启) 2 店铺关闭
if (this.shop.state === 0) return '待审核'
if (this.shop.state === 1) return '店铺开启'
if (this.shop.state === 2) return '店铺关闭'
return '未知状态'
},
shopStateTagType() {
// 标签配色:待审核=warning开启=success关闭=info
if (this.shop.state === 0) return 'warning'
if (this.shop.state === 1) return 'success'
if (this.shop.state === 2) return 'info'
return 'info'
},
hasShop() {
return !!(this.shop && Number(this.shop.id) > 0)
},
canCreateShop() {
return !this.hasShop
},
/**
* 弹窗可选币种:稳定币/虚拟币分流
*/
editCoinOptions() {
if (Array.isArray(this.editCoinOptionsApi) && this.editCoinOptionsApi.length) return this.editCoinOptionsApi
return this.coinOptions
},
selectedCoinLabels() {
const map = new Map((this.editCoinOptions || []).map(o => [String(o.value), String(o.label).toUpperCase()]))
return (this.configForm.payCoins || []).map(v => map.get(String(v)) || String(v).toUpperCase())
}
},
created() {
this.fetchMyShop()
},
methods: {
// 简单的emoji检测覆盖常见表情平面与符号范围
hasEmoji(str) {
if (!str || typeof str !== 'string') return false
const emojiRegex = /[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u2600-\u27BF]/u
return emojiRegex.test(str)
},
/**
* 重置店铺状态为无店铺状态
*/
resetShopState() {
this.shop = {
id: 0,
name: '',
image: '',
description: '',
del: true,
state: 0
}
this.shopConfigs = []
},
async fetchMyShop() {
try {
const res = await getMyShop()
// 预期格式:{"code":0,"data":{"del":true,"description":"","id":0,"image":"","name":"","state":0},"msg":""}
if (res && (res.code === 0 || res.code === 200) && res.data) {
this.shop = {
id: res.data.id,
name: res.data.name,
image: res.data.image,
description: res.data.description,
del: !!res.data.del,
state: Number(res.data.state || 0)
}
// 同步加载钱包绑定
this.fetchShopConfigs(res.data.id)
} else {
// 当接口返回错误或没有数据时,重置店铺状态
this.resetShopState()
if (res && res.msg) {
console.warn('获取店铺数据失败:', res.msg)
}
}
} catch (error) {
console.error('获取店铺信息失败:', error)
// 当接口报错如500错误重置店铺状态
this.resetShopState()
} finally {
this.loaded = true
}
},
async fetchShopConfigs(shopId) {
// 如果店铺ID无效直接清空配置
if (!shopId || shopId <= 0) {
this.shopConfigs = []
return
}
try {
const res = await getShopConfig({id:shopId})
if (res && (res.code === 0 || res.code === 200) && Array.isArray(res.data)) {
// 直接使用后端返回的数据children: [{payCoin,image}]
this.shopConfigs = res.data
} else {
this.shopConfigs = []
}
} catch (e) {
console.warn('获取店铺配置失败:', e)
this.shopConfigs = []
}
},
async updateShopConfig(params) {
const res = await updateShopConfig(params)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('保存成功')
this.visibleConfigEdit = false
this.fetchShopConfigs(this.shop.id)
}
},
async deleteShopConfig(params) {
const res = await deleteShopConfig(params)
if (res && (res.code === 0 || res.code === 200)) {
this.$message.success('删除成功')
this.fetchShopConfigs(this.shop.id)
}
},
async handleEditConfig(row) {
try {
const res = await getChainAndCoin({ id: row.id })
if (res && (res.code === 0 || res.code === 200) && res.data) {
const d = res.data || {}
const children = Array.isArray(d.children) ? d.children : []
this.editCoinOptionsApi = children.map(c => ({ label: c.label, value: c.value }))
const preSelected = children.filter(c => Number(c.hasBind) === 1).map(c => c.value)
this.configForm = {
id: row.id,
chainLabel: d.label || '',
chainValue: d.value || '',
payAddress: d.address || '',
payCoins: preSelected,
payCoin: preSelected.join(',')
}
} else {
// 回退:使用行内已有数据
this.editCoinOptionsApi = []
const chainLabel = row.chain || ''
const payCoinStr = String(row.payCoin || '')
const payCoins = payCoinStr ? payCoinStr.split(',') : []
this.configForm = {
id: row.id,
chainLabel,
chainValue: row.chain || '',
payAddress: row.payAddress || '',
payCoins,
payCoin: payCoins.join(',')
}
}
this.visibleConfigEdit = true
} catch (e) {
this.visibleConfigEdit = true
}
},
async handleDeleteConfig(row) {
this.deleteShopConfig({id:row.id})
},
submitConfigEdit() {
// 基础校验
if (!this.configForm.chainLabel && !this.configForm.chainValue) {
this.$message.warning('请选择支付链')
return
}
if (!this.configForm.payCoins || this.configForm.payCoins.length === 0) {
this.$message.warning('请选择支付币种')
return
}
const addr = (this.configForm.payAddress || '').trim()
if (!addr) {
this.$message.warning('请输入钱包地址')
return
}
const payload = {
id: this.configForm.id,
chain: this.configForm.chainValue || this.configForm.chainLabel,
payCoin: (this.configForm.payCoins || []).join(','),
payAddress: this.configForm.payAddress
}
this.updateShopConfig(payload)
},
removeSelectedCoin(labelUpper) {
const label = String(labelUpper || '').toLowerCase()
const map = new Map((this.editCoinOptions || []).map(o => [String(o.label).toLowerCase(), String(o.value)]))
const value = map.get(label)
if (!value) return
this.configForm.payCoins = (this.configForm.payCoins || []).filter(v => String(v) !== String(value))
},
async handleOpenEdit() {
try {
// 先打开弹窗,提供更快的视觉反馈
this.visibleEdit = true
// 查询最新店铺详情
const res = await queryShop({ id: this.shop.id })
if (res && (res.code === 0 || res.code === 200) && res.data) {
this.editForm = {
id: res.data.id,
name: res.data.name,
image: res.data.image,
description: res.data.description
}
} else {
// 回退到当前展示的数据
this.editForm = {
id: this.shop.id,
name: this.shop.name,
image: this.shop.image,
description: this.shop.description
}
this.$message.warning(res && res.msg ? res.msg : '未获取到店铺详情')
}
} catch (error) {
// 出错时回退到当前展示的数据
this.editForm = {
id: this.shop.id,
name: this.shop.name,
image: this.shop.image,
description: this.shop.description
}
console.error('查询店铺详情失败:', error)
}
},
/**
* 提交店铺修改
* 规则:允许输入空格,但不允许内容为“全是空格”
*/
async submitEdit() {
try {
const { name, image, description } = this.editForm
const isOnlySpaces = (v) => typeof v === 'string' && v.length > 0 && v.trim().length === 0
if (isOnlySpaces(name)) {
this.$message.error('店铺名称不能全是空格')
return
}
if (!name) {
this.$message.error('店铺名称不能为空')
return
}
if (this.hasEmoji(name)) {
this.$message.warning('店铺名称不能包含表情符号')
return
}
if (isOnlySpaces(image)) {
this.$message.error('店铺封面不能全是空格')
return
}
if (isOnlySpaces(description)) {
this.$message.error('店铺描述不能全是空格')
return
}
// 长度限制名称≤30描述≤300
if (name && name.length > 30) {
this.$message.warning('店铺名称不能超过30个字符')
return
}
if (description && description.length > 300) {
this.$message.warning('店铺描述不能超过300个字符')
return
}
const payload = { ...this.editForm }
const res = await updateShop(payload)
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
message: '已保存',
type: 'success',
showClose: true
})
this.visibleEdit = false
this.fetchMyShop()
} else {
this.$message({
message: res.msg || '保存失败',
type: 'error',
showClose: true
})
}
} catch (error) {
console.error('更新店铺失败:', error)
console.log('更新店铺失败,请稍后重试')
}
},
async handleDelete() {
try {
await this.$confirm('确定删除该店铺吗?此操作不可恢复', '提示', { type: 'warning' })
const res = await deleteShop(this.shop.id)
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
message: '删除成功',
type: 'success',
showClose: true
})
// 删除成功后,先重置店铺状态,然后尝试重新获取
this.resetShopState()
this.loaded = false
// 延迟一下再尝试获取,给服务器时间处理删除操作
setTimeout(() => {
this.fetchMyShop()
}, 500)
}
} catch (e) {
// 用户取消
}
},
async handleToggleShop() {
try {
const isClosed = this.shop.state === 2
const confirmMsg = isClosed ? '确定开启店铺吗?' : '确定关闭该店铺吗?关闭后用户将无法访问'
await this.$confirm(confirmMsg, '提示', { type: 'warning' })
const res = await closeShop(this.shop.id)
if (res && (res.code === 0 || res.code === 200)) {
this.$message({
message: isClosed ? '店铺已开启' : '店铺已关闭',
type: 'success',
showClose: true
})
this.fetchMyShop()
} else {
// this.$message.error(res && res.msg ? res.msg : '操作失败')
console.log(`操作失败`);
}
} catch (e) {
// 用户取消
}
},
handleGoNew() {
if (!this.canCreateShop) {
this.$message({
message: '每个用户仅允许一个店铺,无法新建',
type: 'warning',
showClose: true
})
return
}
this.$router.push('/account/shop-new')
},
/**
* 跳转到新增商品页面
*/
handleAddProduct() {
if (!this.hasShop) {
this.$message({
message: '请先创建店铺',
type: 'warning',
showClose: true
})
return
}
// 跳转到新增商品页面并传递店铺ID
this.$router.push({
path: '/account/product-new',
query: { shopId: this.shop.id }
})
},
/**
* 跳转到钱包绑定页面
*/
handleWalletBind() {
if (!this.hasShop) {
this.$message({
message: '请先创建店铺',
type: 'warning',
showClose: true
})
return
}
this.$router.push('/account/shop-config')
}
}
}
</script>
<style scoped>
.panel-title { margin: 0 0 12px 0; font-size: 18px; font-weight: 700; }
.shop-card { border-radius: 8px; }
.shop-row { display: grid; grid-template-columns: 120px 1fr; gap: 16px; align-items: center; }
.shop-cover img { width: 120px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid #eee; }
.shop-info { display: flex; flex-direction: column; gap: 8px; }
.shop-title { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 16px; }
.desc { color: #666; }
.meta { color: #999; display: flex; gap: 16px; font-size: 12px; }
.actions { margin-top: 8px; display: flex; gap: 8px; }
.guide-card { border: 1px solid #eef2f7; border-radius: 10px; }
.guide-header { text-align: center; font-weight: 700; color: #2c3e50; background: #f9fafb; border-bottom: 1px solid #eef2f7; padding: 10px 12px; border-radius: 10px 10px 0 0; }
.guide-content { padding: 4px 6px; text-align: left; }
.guide-card .hierarchy { margin: 0 0 8px 0; color: #111827; font-weight: 700; font-size: 14px; }
.guide-steps { margin: 0; padding-left: 18px; color: #374151; }
.guide-steps li { line-height: 1.9; margin: 6px 0; }
.guide-steps b { color: #111827; }
.guide-note { margin-top: 10px; color: #6b7280; font-size: 13px; background: #f9fafb; border: 1px dashed #e5e7eb; padding: 8px 10px; border-radius: 8px; }
.coin-list { display: flex; align-items: center; gap: 8px; }
.coin-img { width: 20px; height: 20px; border-radius: 4px; display: inline-block; }
</style>
<style>
/* 全局弹窗宽度微调(仅当前页面生效)*/
.el-dialog__body .row { margin-bottom: 12px; }
/* 弹窗表单统一对齐与留白优化 */
.el-dialog__body .row {
display: grid;
grid-template-columns: 96px 1fr;
column-gap: 12px;
align-items: center;
}
.el-dialog__body .row .el-radio-group {
display: inline-flex;
align-items: center;
gap: 24px;
padding-left: 0; /* 与输入框左边缘对齐 */
margin-left: 0; /* 去除可能的默认缩进 */
}
.el-dialog__body .label {
text-align: right;
color: #666;
font-weight: 500;
}
.el-dialog__footer {
padding-top: 4px;
}
/* 已选择币种 - 靠左对齐且换行友好 */
.selected-coin-list { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-start; }
.selected-coin-list .el-tag { margin-right: 0; }
</style>