核心语义反转:receive_cost_delta 为压价点数,收号比例 = 主站参考比例 R + 点数, 点数越大号主收号价越低;买家售价锚定主站参考比例推导,不再随收号价联动。 - 迁移 000072:distribution_sites 新增 receive_cost_delta(旧 receive_ratio_delta 保留只读) - receive_pricing:新增 Normalize/ApplySiteReceiveCostDelta;分站定价买家侧改用 R 锚点(referenceSellerCoinBasePrice),快照写入 site_receive_cost_delta 与 pricing_model 标记 - site_receive:新增 resolveSiteReceiveCostDelta 读取链路,旧 ResolveReceiveRatioDelta 保留 - 分成不变:压价差经结算层 profit 自动进入 35/40/25 三方分成池; 货架自主加价仍 100% 归分站,两条边界均以主站为锚 - 权限对齐旧字段:主站全局管理员与分站操作员均可设置 - 前端三端同步:主站 pricing.ts/usePricingCalculator 买卖解耦试算、 主站后台弹窗改「收号压价」价格语义文案、分销后台 SiteSettings 同步 - 测试:单测锚定 R=43 时 cost_delta=3 → 收号 217.4 / 卖价 263.2 不变 / markup 池 30.6→54.9
1013 lines
34 KiB
Vue
1013 lines
34 KiB
Vue
<script setup lang="ts">
|
||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||
import {
|
||
Plus,
|
||
Shop,
|
||
Link,
|
||
DocumentCopy,
|
||
Refresh,
|
||
Clock,
|
||
Picture,
|
||
Upload,
|
||
Coin,
|
||
Message,
|
||
} from '@element-plus/icons-vue'
|
||
import {
|
||
fetchDistributionSites,
|
||
fetchDistributionSiteDefaults,
|
||
onboardDistributionSite,
|
||
syncDistributionSiteAccounts,
|
||
updateDistributionSite,
|
||
fetchDistributionSiteSMSConfig,
|
||
updateDistributionSiteSMSConfig,
|
||
testDistributionSiteSMSConfig,
|
||
type DistributionSite,
|
||
type OnboardDistributionSitePayload,
|
||
type OnboardDistributionSiteResult,
|
||
} from '@/features/admin/api/adminDistribution'
|
||
import { uploadAdminFile } from '@/shared/api/files'
|
||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||
import { formatDateTime } from '@/shared/utils/time'
|
||
import { readError } from '@/shared/utils/error'
|
||
import {
|
||
distributionAdminBaseUrl,
|
||
distributionAdminLoginUrl,
|
||
} from '@/shared/utils/distributionAdmin'
|
||
|
||
const loading = ref(false)
|
||
const sites = ref<DistributionSite[]>([])
|
||
const defaultSettlementHoldDays = ref(7)
|
||
|
||
const onboardDialogVisible = ref(false)
|
||
const submitting = ref(false)
|
||
const onboardFormRef = ref<FormInstance>()
|
||
|
||
const successDialogVisible = ref(false)
|
||
const onboardResult = ref<OnboardDistributionSiteResult | null>(null)
|
||
|
||
const settlementDialogVisible = ref(false)
|
||
const settlementSubmitting = ref(false)
|
||
const settlementForm = reactive({
|
||
site_id: 0,
|
||
site_name: '',
|
||
settlement_hold_days: defaultSettlementHoldDays.value,
|
||
})
|
||
|
||
const receiveRatioDialogVisible = ref(false)
|
||
const receiveRatioSubmitting = ref(false)
|
||
const receiveRatioForm = reactive({
|
||
site_id: 0,
|
||
site_name: '',
|
||
receive_cost_delta: 0,
|
||
})
|
||
|
||
const qrDialogVisible = ref(false)
|
||
const qrSubmitting = ref(false)
|
||
const qrUploading = ref(false)
|
||
const qrForm = reactive({
|
||
site_id: 0,
|
||
site_name: '',
|
||
listing_qrcode_url: '',
|
||
})
|
||
|
||
const smsDialogVisible = ref(false)
|
||
const smsLoading = ref(false)
|
||
const smsSubmitting = ref(false)
|
||
const smsTesting = ref(false)
|
||
const smsTestCooldown = ref(0)
|
||
let smsCooldownTimer: ReturnType<typeof setInterval> | null = null
|
||
|
||
function startSmsCooldown(seconds = 60) {
|
||
smsTestCooldown.value = seconds
|
||
if (smsCooldownTimer) clearInterval(smsCooldownTimer)
|
||
smsCooldownTimer = setInterval(() => {
|
||
if (smsTestCooldown.value > 0) {
|
||
smsTestCooldown.value--
|
||
} else if (smsCooldownTimer) {
|
||
clearInterval(smsCooldownTimer)
|
||
smsCooldownTimer = null
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
if (smsCooldownTimer) clearInterval(smsCooldownTimer)
|
||
})
|
||
|
||
const smsSecretInput = ref('')
|
||
const smsTestPhone = ref('')
|
||
const smsForm = reactive({
|
||
site_id: 0,
|
||
site_name: '',
|
||
enabled: false,
|
||
provider: 'aliyun',
|
||
sign_name: '',
|
||
login_template_code: '',
|
||
access_key_id: '',
|
||
masked_secret: '',
|
||
has_secret: false,
|
||
endpoint: '',
|
||
})
|
||
|
||
function handleOpenSettlement(row: DistributionSite) {
|
||
settlementForm.site_id = row.id
|
||
settlementForm.site_name = row.site_name
|
||
settlementForm.settlement_hold_days = row.settlement_hold_days ?? defaultSettlementHoldDays.value
|
||
settlementDialogVisible.value = true
|
||
}
|
||
|
||
async function handleSettlementSubmit() {
|
||
settlementSubmitting.value = true
|
||
try {
|
||
await updateDistributionSite(settlementForm.site_id, {
|
||
settlement_hold_days: Number(settlementForm.settlement_hold_days),
|
||
})
|
||
ElMessage.success(`【${settlementForm.site_name}】结算冷却期已更新`)
|
||
settlementDialogVisible.value = false
|
||
await loadSites()
|
||
} catch (err) {
|
||
ElMessage.error(readError(err, '更新结算冷却期失败'))
|
||
} finally {
|
||
settlementSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
function handleOpenReceiveRatio(row: DistributionSite) {
|
||
receiveRatioForm.site_id = row.id
|
||
receiveRatioForm.site_name = row.site_name
|
||
receiveRatioForm.receive_cost_delta = Number(row.receive_cost_delta || 0)
|
||
receiveRatioDialogVisible.value = true
|
||
}
|
||
|
||
async function handleReceiveRatioSubmit() {
|
||
receiveRatioSubmitting.value = true
|
||
try {
|
||
await updateDistributionSite(receiveRatioForm.site_id, {
|
||
receive_cost_delta: Number(receiveRatioForm.receive_cost_delta),
|
||
})
|
||
ElMessage.success(`【${receiveRatioForm.site_name}】收号压价已更新`)
|
||
receiveRatioDialogVisible.value = false
|
||
await loadSites()
|
||
} catch (err) {
|
||
ElMessage.error(readError(err, '更新收号比例失败'))
|
||
} finally {
|
||
receiveRatioSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
function handleOpenQrcode(row: DistributionSite) {
|
||
qrForm.site_id = row.id
|
||
qrForm.site_name = row.site_name
|
||
qrForm.listing_qrcode_url = row.listing_qrcode_url || ''
|
||
qrDialogVisible.value = true
|
||
}
|
||
|
||
async function uploadSiteQrcode(file: File) {
|
||
qrUploading.value = true
|
||
try {
|
||
const uploaded = await uploadAdminFile(file, 'qrcode')
|
||
qrForm.listing_qrcode_url = uploaded.url
|
||
ElMessage.success('二维码已上传,保存后生效')
|
||
} catch (err) {
|
||
ElMessage.error(readError(err, '上传二维码失败'))
|
||
} finally {
|
||
qrUploading.value = false
|
||
}
|
||
return false
|
||
}
|
||
|
||
function clearSiteQrcode() {
|
||
qrForm.listing_qrcode_url = ''
|
||
}
|
||
|
||
async function handleQrcodeSubmit() {
|
||
qrSubmitting.value = true
|
||
try {
|
||
await updateDistributionSite(qrForm.site_id, {
|
||
listing_qrcode_url: qrForm.listing_qrcode_url.trim(),
|
||
})
|
||
ElMessage.success(
|
||
qrForm.listing_qrcode_url.trim()
|
||
? `【${qrForm.site_name}】发布群固定二维码已更新`
|
||
: `【${qrForm.site_name}】已清空发布群固定二维码`
|
||
)
|
||
qrDialogVisible.value = false
|
||
await loadSites()
|
||
} catch (err) {
|
||
ElMessage.error(readError(err, '更新发布群二维码失败'))
|
||
} finally {
|
||
qrSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
async function handleOpenSMS(row: DistributionSite) {
|
||
smsForm.site_id = row.id
|
||
smsForm.site_name = row.site_name
|
||
smsSecretInput.value = ''
|
||
smsTestPhone.value = ''
|
||
smsDialogVisible.value = true
|
||
smsLoading.value = true
|
||
try {
|
||
const cfg = await fetchDistributionSiteSMSConfig(row.id)
|
||
smsForm.enabled = cfg.enabled
|
||
smsForm.provider = cfg.provider || 'aliyun'
|
||
smsForm.sign_name = cfg.sign_name || ''
|
||
smsForm.login_template_code = cfg.login_template_code || ''
|
||
smsForm.access_key_id = cfg.access_key_id || ''
|
||
smsForm.masked_secret = cfg.masked_secret || ''
|
||
smsForm.has_secret = cfg.has_secret
|
||
smsForm.endpoint = cfg.endpoint || ''
|
||
} catch (err) {
|
||
ElMessage.error(readError(err, '读取分站短信配置失败'))
|
||
} finally {
|
||
smsLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleSMSSubmit() {
|
||
if (smsForm.enabled) {
|
||
if (!smsForm.sign_name.trim()) {
|
||
ElMessage.warning('启用独立短信时,短信签名不能为空')
|
||
return
|
||
}
|
||
if (!smsForm.login_template_code.trim()) {
|
||
ElMessage.warning('启用独立短信时,登录验证码模板 Code 不能为空')
|
||
return
|
||
}
|
||
if (!smsForm.access_key_id.trim()) {
|
||
ElMessage.warning('启用独立短信时,AccessKey ID 不能为空')
|
||
return
|
||
}
|
||
if (!smsForm.has_secret && !smsSecretInput.value.trim()) {
|
||
ElMessage.warning('启用独立短信时,AccessKey Secret 不能为空')
|
||
return
|
||
}
|
||
}
|
||
smsSubmitting.value = true
|
||
try {
|
||
const payload: {
|
||
enabled: boolean
|
||
provider: string
|
||
sign_name: string
|
||
login_template_code: string
|
||
access_key_id: string
|
||
access_key_secret?: string
|
||
endpoint?: string
|
||
} = {
|
||
enabled: smsForm.enabled,
|
||
provider: smsForm.provider || 'aliyun',
|
||
sign_name: smsForm.sign_name.trim(),
|
||
login_template_code: smsForm.login_template_code.trim(),
|
||
access_key_id: smsForm.access_key_id.trim(),
|
||
endpoint: smsForm.endpoint.trim(),
|
||
}
|
||
if (smsSecretInput.value.trim()) {
|
||
payload.access_key_secret = smsSecretInput.value.trim()
|
||
}
|
||
await updateDistributionSiteSMSConfig(smsForm.site_id, payload)
|
||
ElMessage.success(`【${smsForm.site_name}】短信配置已保存`)
|
||
smsDialogVisible.value = false
|
||
} catch (err) {
|
||
ElMessage.error(readError(err, '保存短信配置失败'))
|
||
} finally {
|
||
smsSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
async function handleSMSTest() {
|
||
const phone = smsTestPhone.value.trim()
|
||
if (!phone) {
|
||
ElMessage.warning('请输入接收测试短信的手机号')
|
||
return
|
||
}
|
||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||
ElMessage.warning('手机号格式不正确,请输入有效的11位手机号码')
|
||
return
|
||
}
|
||
if (!smsForm.access_key_id.trim()) {
|
||
ElMessage.warning('请先填写 AccessKey ID')
|
||
return
|
||
}
|
||
if (!smsForm.has_secret && !smsSecretInput.value.trim()) {
|
||
ElMessage.warning('请先填写 AccessKey Secret')
|
||
return
|
||
}
|
||
if (!smsForm.sign_name.trim()) {
|
||
ElMessage.warning('请先填写短信签名')
|
||
return
|
||
}
|
||
if (!smsForm.login_template_code.trim()) {
|
||
ElMessage.warning('请先填写验证码模板 Code')
|
||
return
|
||
}
|
||
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`将向手机号 ${phone} 发送一条测试验证码,并消耗该站点的阿里云短信额度,是否继续?`,
|
||
'确认发送测试短信',
|
||
{
|
||
confirmButtonText: '确认发送',
|
||
cancelButtonText: '取消',
|
||
type: 'info',
|
||
}
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
|
||
smsTesting.value = true
|
||
try {
|
||
const payload = {
|
||
phone,
|
||
enabled: true,
|
||
provider: smsForm.provider || 'aliyun',
|
||
sign_name: smsForm.sign_name.trim(),
|
||
login_template_code: smsForm.login_template_code.trim(),
|
||
access_key_id: smsForm.access_key_id.trim(),
|
||
endpoint: smsForm.endpoint.trim() || undefined,
|
||
access_key_secret: smsSecretInput.value.trim() || undefined,
|
||
}
|
||
await testDistributionSiteSMSConfig(smsForm.site_id, payload)
|
||
ElMessage.success('测试短信发送成功,请查收手机短信')
|
||
startSmsCooldown(60)
|
||
} catch (err) {
|
||
ElMessage.error(readError(err, '测试短信发送失败'))
|
||
} finally {
|
||
smsTesting.value = false
|
||
}
|
||
}
|
||
|
||
const onboardForm = reactive({
|
||
site_name: '',
|
||
site_code: '',
|
||
domain: '',
|
||
admin_username: '',
|
||
admin_password: '',
|
||
admin_nickname: '',
|
||
member_type: 'operator',
|
||
settlement_hold_days: defaultSettlementHoldDays.value,
|
||
display_account_enabled: true,
|
||
tax_enabled: false,
|
||
tax_rate_bps: 100,
|
||
})
|
||
|
||
const onboardRules: FormRules = {
|
||
site_name: [{ required: true, message: '请输入分销站点名称', trigger: 'blur' }],
|
||
site_code: [
|
||
{ required: true, message: '请输入站点唯一标识', trigger: 'blur' },
|
||
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '仅支持英文字母、数字及下划线', trigger: 'blur' },
|
||
],
|
||
domain: [{ required: true, message: '请输入分销专属域名', trigger: 'blur' }],
|
||
admin_username: [{ required: true, message: '请输入分销管理员账号', trigger: 'blur' }],
|
||
admin_password: [
|
||
{ required: true, message: '请输入初始密码', trigger: 'blur' },
|
||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' },
|
||
],
|
||
}
|
||
|
||
async function loadSites() {
|
||
loading.value = true
|
||
try {
|
||
sites.value = await fetchDistributionSites()
|
||
} catch (err) {
|
||
ElMessage.error(readError(err, '获取分销站点列表失败'))
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleOpenOnboard() {
|
||
onboardForm.site_name = ''
|
||
onboardForm.site_code = ''
|
||
onboardForm.domain = ''
|
||
onboardForm.admin_username = ''
|
||
onboardForm.admin_password = ''
|
||
onboardForm.admin_nickname = ''
|
||
onboardForm.member_type = 'operator'
|
||
onboardForm.settlement_hold_days = defaultSettlementHoldDays.value
|
||
onboardForm.display_account_enabled = true
|
||
onboardForm.tax_enabled = false
|
||
onboardForm.tax_rate_bps = 100
|
||
onboardDialogVisible.value = true
|
||
}
|
||
|
||
async function handleOnboardSubmit() {
|
||
if (!onboardFormRef.value) return
|
||
await onboardFormRef.value.validate(async valid => {
|
||
if (!valid) return
|
||
submitting.value = true
|
||
try {
|
||
const payload: OnboardDistributionSitePayload = {
|
||
site_name: onboardForm.site_name.trim(),
|
||
site_code: onboardForm.site_code.trim(),
|
||
domain: onboardForm.domain.trim(),
|
||
admin_username: onboardForm.admin_username.trim(),
|
||
admin_password: onboardForm.admin_password,
|
||
admin_nickname: onboardForm.admin_nickname.trim() || undefined,
|
||
member_type: onboardForm.member_type,
|
||
settlement_hold_days: Number(onboardForm.settlement_hold_days),
|
||
display_account_enabled: onboardForm.display_account_enabled,
|
||
tax_enabled: onboardForm.tax_enabled,
|
||
tax_rate_bps: onboardForm.tax_enabled ? onboardForm.tax_rate_bps : 0,
|
||
}
|
||
const res = await onboardDistributionSite(payload)
|
||
ElMessage.success('分销站点与管理员账号已成功开通!')
|
||
onboardDialogVisible.value = false
|
||
onboardResult.value = res
|
||
successDialogVisible.value = true
|
||
await loadSites()
|
||
} catch (err) {
|
||
ElMessage.error(readError(err, '开通分销商失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
function handleCopyInfo() {
|
||
if (!onboardResult.value) return
|
||
const text = `分销站点:${onboardResult.value.site.site_name} (${onboardResult.value.site.site_code})\n分销域名:${onboardResult.value.site.domain}\n后台登录地址:${distributionAdminLoginUrl()}\n管理员账号:${onboardResult.value.member.username}`
|
||
navigator.clipboard?.writeText(text)
|
||
ElMessage.success('已复制分销商开通凭证到剪贴板')
|
||
}
|
||
|
||
function openDistributionAdmin() {
|
||
window.open(distributionAdminBaseUrl(), '_blank')
|
||
}
|
||
|
||
const syncingSiteId = ref<number | null>(null)
|
||
|
||
async function handleSyncAccounts(row: DistributionSite) {
|
||
syncingSiteId.value = row.id
|
||
try {
|
||
const res = await syncDistributionSiteAccounts(row.id)
|
||
ElMessage.success(
|
||
`同步成功!为【${row.site_name}】新增本站上传货 ${res.synced_count} 个,已存在 ${res.skipped_count} 个(本次候选 ${res.total_active} 个)`
|
||
)
|
||
} catch (err) {
|
||
ElMessage.error(readError(err, '同步商品失败'))
|
||
} finally {
|
||
syncingSiteId.value = null
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadSites()
|
||
fetchDistributionSiteDefaults()
|
||
.then(res => {
|
||
const days = res?.default_settlement_hold_days
|
||
if (typeof days === 'number' && days >= 0 && days <= 365) {
|
||
defaultSettlementHoldDays.value = days
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// 拉取失败时保持内置默认值 7,不弹错误提示
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="distribution-sites-view">
|
||
<div class="page-header">
|
||
<div>
|
||
<h2 class="page-title">分销站点管理</h2>
|
||
<p class="page-subtitle">
|
||
开通分销站、配置结算冷却期、收号比例上限与发布群固定二维码。利润按 35% 收号 / 40% 卖出 /
|
||
25% 服务费切分。
|
||
</p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<el-button :icon="Link" @click="openDistributionAdmin"> 打开独立分销后台 </el-button>
|
||
<el-button type="primary" :icon="Plus" @click="handleOpenOnboard">
|
||
一键开通分销商
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-card shadow="never">
|
||
<el-table v-loading="loading" :data="sites" stripe style="width: 100%">
|
||
<el-table-column label="站点名称" prop="site_name" min-width="160">
|
||
<template #default="{ row }">
|
||
<div class="site-name-cell">
|
||
<el-icon class="site-icon"><Shop /></el-icon>
|
||
<strong>{{ row.site_name }}</strong>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="站点标识" prop="site_code" min-width="120">
|
||
<template #default="{ row }">
|
||
<el-tag size="small">{{ row.site_code }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="绑定域名" prop="domain" min-width="180" />
|
||
<el-table-column label="运行状态" prop="status" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 'active' ? 'success' : 'info'" size="small">
|
||
{{ row.status === 'active' ? '正常' : '已停用' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="展示主站账号" prop="display_account_enabled" width="130">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.display_account_enabled ? 'success' : 'info'" size="small">
|
||
{{ row.display_account_enabled ? '已开启' : '关闭' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="税费设定" width="120">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.tax_enabled" type="warning" size="small">
|
||
{{ (row.tax_rate_bps / 100).toFixed(1) }}%
|
||
</el-tag>
|
||
<el-tag v-else type="info" size="small">免税</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="结算冷却期" width="110">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.settlement_hold_days > 0 ? 'primary' : 'success'" size="small">
|
||
{{ row.settlement_hold_days > 0 ? `${row.settlement_hold_days} 天` : '立即释放' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="收号压价" width="110">
|
||
<template #default="{ row }">
|
||
<el-tag
|
||
:type="Number(row.receive_cost_delta || 0) > 0 ? 'warning' : 'info'"
|
||
size="small"
|
||
>
|
||
{{
|
||
Number(row.receive_cost_delta || 0) > 0
|
||
? `低 ${Number(row.receive_cost_delta).toFixed(1)} 点`
|
||
: '与主站一致'
|
||
}}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="发布群二维码" width="120" align="center">
|
||
<template #default="{ row }">
|
||
<AuthImage
|
||
v-if="row.listing_qrcode_url"
|
||
:source="row.listing_qrcode_url"
|
||
fit="cover"
|
||
:image-style="{ width: '48px', height: '48px', borderRadius: '6px' }"
|
||
:preview-src-list="[row.listing_qrcode_url]"
|
||
/>
|
||
<el-tag v-else type="info" size="small">未配置</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="创建时间" prop="created_at" min-width="160">
|
||
<template #default="{ row }">
|
||
{{ formatDateTime(row.created_at) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="410" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
:icon="Refresh"
|
||
:loading="syncingSiteId === row.id"
|
||
@click="handleSyncAccounts(row)"
|
||
>
|
||
同步商品
|
||
</el-button>
|
||
<el-button type="primary" link :icon="Clock" @click="handleOpenSettlement(row)">
|
||
结算设置
|
||
</el-button>
|
||
<el-button type="primary" link :icon="Coin" @click="handleOpenReceiveRatio(row)">
|
||
收号比例
|
||
</el-button>
|
||
<el-button type="primary" link :icon="Picture" @click="handleOpenQrcode(row)">
|
||
二维码
|
||
</el-button>
|
||
<el-button type="primary" link :icon="Message" @click="handleOpenSMS(row)">
|
||
短信配置
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<!-- 一键开通分销商对话框 -->
|
||
<el-dialog v-model="onboardDialogVisible" title="一键开通分销商" width="680px" destroy-on-close>
|
||
<el-form ref="onboardFormRef" :model="onboardForm" :rules="onboardRules" label-width="140px">
|
||
<div class="form-section-title">1. 分销站点基础信息</div>
|
||
<el-row :gutter="16">
|
||
<el-col :span="12">
|
||
<el-form-item label="站点名称" prop="site_name">
|
||
<el-input v-model="onboardForm.site_name" placeholder="例如:广州游戏分销站" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="站点唯一标识" prop="site_code">
|
||
<el-input v-model="onboardForm.site_code" placeholder="例如:gz_dist" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-form-item label="分销专属域名" prop="domain">
|
||
<el-input v-model="onboardForm.domain" placeholder="例如:gz.distribution.hao858.com" />
|
||
</el-form-item>
|
||
|
||
<div class="form-section-title">2. 分销商管理员账号</div>
|
||
<el-row :gutter="16">
|
||
<el-col :span="12">
|
||
<el-form-item label="登录用户名" prop="admin_username">
|
||
<el-input v-model="onboardForm.admin_username" placeholder="例如:gz_admin" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="初始登录密码" prop="admin_password">
|
||
<el-input
|
||
v-model="onboardForm.admin_password"
|
||
type="password"
|
||
show-password
|
||
placeholder="至少6位"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="16">
|
||
<el-col :span="12">
|
||
<el-form-item label="站长姓名/昵称" prop="admin_nickname">
|
||
<el-input v-model="onboardForm.admin_nickname" placeholder="例如:张站长" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="初始角色权限" prop="member_type">
|
||
<el-select v-model="onboardForm.member_type" style="width: 100%">
|
||
<el-option label="分销运营 (operator)" value="operator" />
|
||
<el-option label="分销财务 (finance)" value="finance" />
|
||
<el-option label="客服人员 (customer_service)" value="customer_service" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<div class="form-section-title">3. 结算冷却期与展示</div>
|
||
<el-alert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
title="开通后四种分成模式均按默认生效:收号 35% / 卖出 40% / 服务费 25%(本站自卖为 75% + 25%)。之后可在「分销规则」页单独调整。"
|
||
style="margin-bottom: 12px"
|
||
/>
|
||
<el-row :gutter="16">
|
||
<el-col :span="12">
|
||
<el-form-item label="结算冷却期 (天)">
|
||
<el-input-number
|
||
v-model="onboardForm.settlement_hold_days"
|
||
:min="0"
|
||
:max="365"
|
||
:step="1"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="展示主站账号">
|
||
<el-switch v-model="onboardForm.display_account_enabled" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-form-item label="是否征税">
|
||
<el-switch v-model="onboardForm.tax_enabled" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="onboardDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="handleOnboardSubmit">
|
||
立即开通并生成分销商
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 开通成功凭证对话框 -->
|
||
<el-dialog v-model="successDialogVisible" title="分销商开通成功" width="560px">
|
||
<div v-if="onboardResult">
|
||
<el-alert
|
||
type="success"
|
||
:closable="false"
|
||
show-icon
|
||
title="分销站点与管理员账号已成功初始化,请妥善保管交付凭证:"
|
||
style="margin-bottom: 16px"
|
||
/>
|
||
<el-descriptions :column="1" border size="small">
|
||
<el-descriptions-item label="分销站点">
|
||
<strong>{{ onboardResult.site.site_name }}</strong> ({{ onboardResult.site.site_code }})
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="分销专属域名">
|
||
{{ onboardResult.site.domain }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="分销后台登录账号">
|
||
<el-tag type="success">{{ onboardResult.member.username }}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="分销后台地址">
|
||
<code>{{ distributionAdminLoginUrl() }}</code>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="初始分成规则">
|
||
四种模式均已按默认生效:收号 35% / 卖出 40% / 服务费 25%(本站自卖 75% +
|
||
25%),可到规则页调整。
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="结算冷却期">
|
||
{{ onboardResult.site.settlement_hold_days }} 天
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</div>
|
||
<template #footer>
|
||
<el-button :icon="DocumentCopy" type="primary" @click="handleCopyInfo">
|
||
一键复制交付信息
|
||
</el-button>
|
||
<el-button @click="successDialogVisible = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 结算冷却期设置对话框 -->
|
||
<el-dialog
|
||
v-model="settlementDialogVisible"
|
||
title="结算冷却期设置"
|
||
width="480px"
|
||
destroy-on-close
|
||
>
|
||
<el-alert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
title="订单完成后进入冷却期,期满自动释放到商户可提现余额;0 天表示完成即释放。修改后对尚未释放的已完成订单立即生效。"
|
||
style="margin-bottom: 16px"
|
||
/>
|
||
<el-form label-width="110px">
|
||
<el-form-item label="分销站点">
|
||
<el-input :model-value="settlementForm.site_name" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="冷却天数">
|
||
<el-input-number
|
||
v-model="settlementForm.settlement_hold_days"
|
||
:min="0"
|
||
:max="365"
|
||
:step="1"
|
||
/>
|
||
<span class="settlement-hint">天(0-365)</span>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="settlementDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="settlementSubmitting" @click="handleSettlementSubmit">
|
||
保存设置
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 分站收号压价(收号价始终不高于主站参考价) -->
|
||
<el-dialog
|
||
v-model="receiveRatioDialogVisible"
|
||
title="分站收号压价"
|
||
width="520px"
|
||
destroy-on-close
|
||
>
|
||
<el-alert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
title="分站收号价比主站更低:压价点数越大,号主在分站拿到的收号价越低。买家售价保持与主站一致,压价差进入三方分成(35% 收号 / 40% 卖出 / 25% 服务费)。0 表示与主站同价收号。"
|
||
style="margin-bottom: 16px"
|
||
/>
|
||
<el-form label-width="110px">
|
||
<el-form-item label="分销站点">
|
||
<el-input :model-value="receiveRatioForm.site_name" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="收号价比主站低">
|
||
<el-input-number
|
||
v-model="receiveRatioForm.receive_cost_delta"
|
||
:min="0"
|
||
:max="20"
|
||
:step="0.1"
|
||
:precision="1"
|
||
/>
|
||
<span class="settlement-hint">点(0-20)</span>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="receiveRatioDialogVisible = false">取消</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:loading="receiveRatioSubmitting"
|
||
@click="handleReceiveRatioSubmit"
|
||
>
|
||
保存设置
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 发布群固定二维码 -->
|
||
<el-dialog v-model="qrDialogVisible" title="发布群固定二维码" width="520px" destroy-on-close>
|
||
<el-alert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
title="号主在该分站上传账号后,会把这张固定二维码发到对应发布群。同一分站可重复使用,不消耗主站二维码池。"
|
||
style="margin-bottom: 16px"
|
||
/>
|
||
<el-form label-width="110px">
|
||
<el-form-item label="分销站点">
|
||
<el-input :model-value="qrForm.site_name" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="固定二维码">
|
||
<div class="qr-row">
|
||
<AuthImage
|
||
v-if="qrForm.listing_qrcode_url"
|
||
:source="qrForm.listing_qrcode_url"
|
||
fit="contain"
|
||
:image-style="{
|
||
width: '160px',
|
||
height: '160px',
|
||
borderRadius: '12px',
|
||
background: '#f7f9fc',
|
||
}"
|
||
:preview-src-list="[qrForm.listing_qrcode_url]"
|
||
/>
|
||
<div v-else class="qr-empty">尚未配置</div>
|
||
<div class="qr-actions">
|
||
<el-upload
|
||
:show-file-list="false"
|
||
accept="image/*"
|
||
:disabled="qrUploading"
|
||
:before-upload="uploadSiteQrcode"
|
||
>
|
||
<el-button :icon="Upload" :loading="qrUploading">上传二维码</el-button>
|
||
</el-upload>
|
||
<el-button :disabled="!qrForm.listing_qrcode_url" @click="clearSiteQrcode">
|
||
清空
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="qrDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="qrSubmitting" @click="handleQrcodeSubmit">
|
||
保存设置
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 短信配置对话框 -->
|
||
<el-dialog
|
||
v-model="smsDialogVisible"
|
||
title="分销站点独立短信服务配置"
|
||
width="620px"
|
||
destroy-on-close
|
||
>
|
||
<div v-loading="smsLoading">
|
||
<el-alert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
title="启用后,该分站用户的登录注册验证码将使用独立的签名与模板,并直接计入该分站的阿里云短信额度;未启用时自动回退使用主站默认短信通道。"
|
||
style="margin-bottom: 16px"
|
||
/>
|
||
<el-form label-width="130px">
|
||
<el-form-item label="分销站点">
|
||
<el-input :model-value="smsForm.site_name" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="启用独立短信">
|
||
<el-switch v-model="smsForm.enabled" />
|
||
<span style="margin-left: 12px; font-size: 13px; color: #6b7280">
|
||
{{ smsForm.enabled ? '已启用(优先使用分站凭据)' : '未启用(回退使用主站额度)' }}
|
||
</span>
|
||
</el-form-item>
|
||
<el-form-item label="短信服务商">
|
||
<el-select v-model="smsForm.provider" style="width: 100%">
|
||
<el-option label="阿里云短信服务 (Aliyun Dysms)" value="aliyun" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="短信签名" :required="smsForm.enabled">
|
||
<el-input v-model="smsForm.sign_name" placeholder="例如:XX租号" />
|
||
</el-form-item>
|
||
<el-form-item label="验证码模板 Code" :required="smsForm.enabled">
|
||
<el-input v-model="smsForm.login_template_code" placeholder="例如:SMS_123456789" />
|
||
</el-form-item>
|
||
<el-form-item label="AccessKey ID" :required="smsForm.enabled">
|
||
<el-input v-model="smsForm.access_key_id" placeholder="例如:LTAI5t..." />
|
||
</el-form-item>
|
||
<el-form-item label="AccessKey Secret" :required="smsForm.enabled && !smsForm.has_secret">
|
||
<el-input
|
||
v-model="smsSecretInput"
|
||
type="password"
|
||
show-password
|
||
:placeholder="
|
||
smsForm.has_secret
|
||
? `已设置密钥 (${smsForm.masked_secret}),留空保持不变`
|
||
: '请输入 AccessKey Secret'
|
||
"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="API Endpoint">
|
||
<el-input v-model="smsForm.endpoint" placeholder="默认 dysmsapi.aliyuncs.com" />
|
||
</el-form-item>
|
||
<el-form-item label="连通性测试">
|
||
<div style="display: flex; gap: 8px; width: 100%">
|
||
<el-input v-model="smsTestPhone" placeholder="输入接收测试短信的手机号" />
|
||
<el-button
|
||
:loading="smsTesting"
|
||
:disabled="smsTestCooldown > 0"
|
||
@click="handleSMSTest"
|
||
>
|
||
{{ smsTestCooldown > 0 ? `重新发送(${smsTestCooldown}s)` : '发送测试' }}
|
||
</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="smsDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="smsSubmitting" @click="handleSMSSubmit">
|
||
保存设置
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.distribution-sites-view {
|
||
padding: 20px;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
margin: 0 0 6px 0;
|
||
color: #1f2937;
|
||
}
|
||
|
||
.page-subtitle {
|
||
font-size: 13px;
|
||
color: #6b7280;
|
||
margin: 0;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.site-name-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.site-icon {
|
||
color: #409eff;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.form-section-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
margin: 16px 0 12px 0;
|
||
padding-bottom: 6px;
|
||
border-bottom: 1px dashed #e5e7eb;
|
||
}
|
||
|
||
.settlement-hint {
|
||
margin-left: 8px;
|
||
color: #6b7280;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.qr-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.qr-empty {
|
||
width: 160px;
|
||
height: 160px;
|
||
border-radius: 12px;
|
||
background: #f7f9fc;
|
||
color: #9ca3af;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.qr-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
</style>
|