chore: update auth and portal
This commit is contained in:
@@ -1,118 +1,7 @@
|
||||
<template>
|
||||
<!-- Main Container -->
|
||||
<div class="mx-auto max-w-4xl my-8 bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden min-h-[600px]">
|
||||
|
||||
<!-- Step 1: Landing -->
|
||||
<div v-if="step === 1">
|
||||
<div class="relative h-[400px] w-full overflow-hidden">
|
||||
<img src="https://images.unsplash.com/photo-1522202176988-66273c2fd55f?ixlib=rb-1.2.1&auto=format&fit=crop&w=1200&q=80" class="w-full h-full object-cover">
|
||||
<div class="absolute inset-0 bg-black/40 flex items-center justify-center p-12">
|
||||
<div class="text-center text-white">
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-4 drop-shadow-lg">开启您的内容创作之旅</h1>
|
||||
<p class="text-xl md:text-2xl opacity-90 max-w-2xl mx-auto drop-shadow-md">加入我们,获得专属频道主页,通过优质戏曲内容获取收益,与百万戏迷互动。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
|
||||
<div class="p-6 bg-slate-50 rounded-xl text-center">
|
||||
<div class="w-12 h-12 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-2xl mx-auto mb-4"><i class="pi pi-wallet"></i></div>
|
||||
<h3 class="font-bold text-slate-900 mb-2">内容变现</h3>
|
||||
<p class="text-sm text-slate-500">灵活设置付费阅读与会员专栏,收益直接结算。</p>
|
||||
</div>
|
||||
<div class="p-6 bg-slate-50 rounded-xl text-center">
|
||||
<div class="w-12 h-12 bg-purple-100 text-purple-600 rounded-full flex items-center justify-center text-2xl mx-auto mb-4"><i class="pi pi-home"></i></div>
|
||||
<h3 class="font-bold text-slate-900 mb-2">专属频道</h3>
|
||||
<p class="text-sm text-slate-500">拥有独立的品牌展示空间,积累私域粉丝。</p>
|
||||
</div>
|
||||
<div class="p-6 bg-slate-50 rounded-xl text-center">
|
||||
<div class="w-12 h-12 bg-orange-100 text-orange-600 rounded-full flex items-center justify-center text-2xl mx-auto mb-4"><i class="pi pi-chart-bar"></i></div>
|
||||
<h3 class="font-bold text-slate-900 mb-2">数据管理</h3>
|
||||
<p class="text-sm text-slate-500">全方位的作品数据分析,助您优化创作方向。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button @click="step = 2" class="px-12 py-4 bg-primary-600 text-white rounded-full font-bold text-xl hover:bg-primary-700 transition-colors shadow-lg shadow-primary-200 cursor-pointer active:scale-95">
|
||||
立即申请入驻
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Application Form -->
|
||||
<div v-else-if="step === 2" class="p-8 md:p-12">
|
||||
<h2 class="text-2xl font-bold text-slate-900 mb-8">填写申请资料</h2>
|
||||
|
||||
<div class="space-y-8 max-w-2xl mx-auto">
|
||||
<!-- Avatar -->
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="w-24 h-24 rounded-full bg-slate-100 flex items-center justify-center border-2 border-dashed border-slate-300 hover:border-primary-400 cursor-pointer transition-colors" @click="triggerUpload">
|
||||
<img v-if="form.avatar" :src="form.avatar" class="w-full h-full rounded-full object-cover">
|
||||
<i v-else class="pi pi-camera text-3xl text-slate-400"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-slate-900 mb-1">频道头像 <span class="text-red-500">*</span></div>
|
||||
<p class="text-sm text-slate-500">建议使用清晰的个人照或 Logo</p>
|
||||
</div>
|
||||
<input type="file" ref="fileInput" class="hidden" @change="handleFileChange">
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label class="block font-bold text-slate-900 mb-2">频道名称 <span class="text-red-500">*</span></label>
|
||||
<input v-model="form.name" type="text" class="w-full h-12 px-4 rounded-lg border border-slate-200 focus:border-primary-500 focus:ring-4 focus:ring-primary-100 outline-none transition-all text-lg" placeholder="给您的频道起个名字">
|
||||
</div>
|
||||
|
||||
<!-- Bio -->
|
||||
<div>
|
||||
<label class="block font-bold text-slate-900 mb-2">频道介绍 <span class="text-red-500">*</span></label>
|
||||
<textarea v-model="form.bio" rows="3" class="w-full p-4 rounded-lg border border-slate-200 focus:border-primary-500 focus:ring-4 focus:ring-primary-100 outline-none transition-all text-base resize-none" placeholder="一句话介绍您的频道特色..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Agreement -->
|
||||
<div class="flex items-center pt-4">
|
||||
<input type="checkbox" id="agree" v-model="form.agreed" class="w-5 h-5 rounded border-slate-300 text-primary-600 focus:ring-primary-500 cursor-pointer">
|
||||
<label for="agree" class="ml-3 text-slate-600 cursor-pointer">
|
||||
我已阅读并同意 <a href="#" class="text-primary-600 hover:underline">《创作者入驻协议》</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="pt-6">
|
||||
<button
|
||||
@click="submitForm"
|
||||
class="w-full py-4 bg-primary-600 text-white rounded-xl font-bold text-xl hover:bg-primary-700 transition-all shadow-lg disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer active:scale-95"
|
||||
:disabled="!isValid"
|
||||
>
|
||||
{{ submitting ? '正在提交...' : '提交审核' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Result -->
|
||||
<div v-else-if="step === 3" class="p-20 text-center">
|
||||
<div class="w-20 h-20 bg-green-100 text-green-600 rounded-full flex items-center justify-center text-4xl mx-auto mb-6 animate-in zoom-in duration-300">
|
||||
<i class="pi pi-check"></i>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-slate-900 mb-4">申请已提交</h2>
|
||||
<p class="text-lg text-slate-600 mb-12 max-w-lg mx-auto">您的入驻申请已成功提交,平台将在 1-3 个工作日内完成审核。审核结果将通过短信和系统通知发送给您。</p>
|
||||
|
||||
<div class="flex justify-center gap-4">
|
||||
<router-link :to="tenantRoute('/')" class="px-8 py-3 bg-white border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 font-medium">返回首页</router-link>
|
||||
<router-link :to="tenantRoute('/me')" class="px-8 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 font-medium">查看个人中心</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { tenantPath } from "../../utils/tenant";
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
@@ -121,39 +10,231 @@ const submitting = ref(false);
|
||||
const fileInput = ref(null);
|
||||
|
||||
const form = reactive({
|
||||
avatar: '',
|
||||
name: '',
|
||||
bio: '',
|
||||
agreed: false
|
||||
avatar: "",
|
||||
name: "",
|
||||
bio: "",
|
||||
agreed: false,
|
||||
});
|
||||
|
||||
const isValid = computed(() => {
|
||||
return form.avatar && form.name && form.bio && form.agreed;
|
||||
return form.avatar && form.name && form.bio && form.agreed;
|
||||
});
|
||||
|
||||
const triggerUpload = () => {
|
||||
fileInput.value.click();
|
||||
fileInput.value.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
form.avatar = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
form.avatar = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
if (!isValid.value) return;
|
||||
|
||||
submitting.value = true;
|
||||
// Simulate API
|
||||
setTimeout(() => {
|
||||
submitting.value = false;
|
||||
step.value = 3;
|
||||
}, 1500);
|
||||
if (!isValid.value) return;
|
||||
|
||||
submitting.value = true;
|
||||
// Simulate API
|
||||
setTimeout(() => {
|
||||
submitting.value = false;
|
||||
step.value = 3;
|
||||
}, 1500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Main Container -->
|
||||
<div
|
||||
class="mx-auto max-w-4xl my-8 bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden min-h-[600px]"
|
||||
>
|
||||
<!-- Step 1: Landing -->
|
||||
<div v-if="step === 1">
|
||||
<div class="relative h-[400px] w-full overflow-hidden">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1522202176988-66273c2fd55f?ixlib=rb-1.2.1&auto=format&fit=crop&w=1200&q=80"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 bg-black/40 flex items-center justify-center p-12"
|
||||
>
|
||||
<div class="text-center text-white">
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-4 drop-shadow-lg">
|
||||
开启您的内容创作之旅
|
||||
</h1>
|
||||
<p
|
||||
class="text-xl md:text-2xl opacity-90 max-w-2xl mx-auto drop-shadow-md"
|
||||
>
|
||||
加入我们,获得专属频道主页,通过优质戏曲内容获取收益,与百万戏迷互动。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
|
||||
<div class="p-6 bg-slate-50 rounded-xl text-center">
|
||||
<div
|
||||
class="w-12 h-12 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-2xl mx-auto mb-4"
|
||||
>
|
||||
<i class="pi pi-wallet"></i>
|
||||
</div>
|
||||
<h3 class="font-bold text-slate-900 mb-2">内容变现</h3>
|
||||
<p class="text-sm text-slate-500">
|
||||
灵活设置付费阅读与会员专栏,收益直接结算。
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6 bg-slate-50 rounded-xl text-center">
|
||||
<div
|
||||
class="w-12 h-12 bg-purple-100 text-purple-600 rounded-full flex items-center justify-center text-2xl mx-auto mb-4"
|
||||
>
|
||||
<i class="pi pi-home"></i>
|
||||
</div>
|
||||
<h3 class="font-bold text-slate-900 mb-2">专属频道</h3>
|
||||
<p class="text-sm text-slate-500">
|
||||
拥有独立的品牌展示空间,积累私域粉丝。
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6 bg-slate-50 rounded-xl text-center">
|
||||
<div
|
||||
class="w-12 h-12 bg-orange-100 text-orange-600 rounded-full flex items-center justify-center text-2xl mx-auto mb-4"
|
||||
>
|
||||
<i class="pi pi-chart-bar"></i>
|
||||
</div>
|
||||
<h3 class="font-bold text-slate-900 mb-2">数据管理</h3>
|
||||
<p class="text-sm text-slate-500">
|
||||
全方位的作品数据分析,助您优化创作方向。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<button
|
||||
@click="step = 2"
|
||||
class="px-12 py-4 bg-primary-600 text-white rounded-full font-bold text-xl hover:bg-primary-700 transition-colors shadow-lg shadow-primary-200 cursor-pointer active:scale-95"
|
||||
>
|
||||
立即申请入驻
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Application Form -->
|
||||
<div v-else-if="step === 2" class="p-8 md:p-12">
|
||||
<h2 class="text-2xl font-bold text-slate-900 mb-8">填写申请资料</h2>
|
||||
|
||||
<div class="space-y-8 max-w-2xl mx-auto">
|
||||
<!-- Avatar -->
|
||||
<div class="flex items-center gap-6">
|
||||
<div
|
||||
class="w-24 h-24 rounded-full bg-slate-100 flex items-center justify-center border-2 border-dashed border-slate-300 hover:border-primary-400 cursor-pointer transition-colors"
|
||||
@click="triggerUpload"
|
||||
>
|
||||
<img
|
||||
v-if="form.avatar"
|
||||
:src="form.avatar"
|
||||
class="w-full h-full rounded-full object-cover"
|
||||
/>
|
||||
<i v-else class="pi pi-camera text-3xl text-slate-400"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-slate-900 mb-1">
|
||||
频道头像 <span class="text-red-500">*</span>
|
||||
</div>
|
||||
<p class="text-sm text-slate-500">建议使用清晰的个人照或 Logo</p>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
class="hidden"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label class="block font-bold text-slate-900 mb-2"
|
||||
>频道名称 <span class="text-red-500">*</span></label
|
||||
>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="w-full h-12 px-4 rounded-lg border border-slate-200 focus:border-primary-500 focus:ring-4 focus:ring-primary-100 outline-none transition-all text-lg"
|
||||
placeholder="给您的频道起个名字"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Bio -->
|
||||
<div>
|
||||
<label class="block font-bold text-slate-900 mb-2"
|
||||
>频道介绍 <span class="text-red-500">*</span></label
|
||||
>
|
||||
<textarea
|
||||
v-model="form.bio"
|
||||
rows="3"
|
||||
class="w-full p-4 rounded-lg border border-slate-200 focus:border-primary-500 focus:ring-4 focus:ring-primary-100 outline-none transition-all text-base resize-none"
|
||||
placeholder="一句话介绍您的频道特色..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Agreement -->
|
||||
<div class="flex items-center pt-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="agree"
|
||||
v-model="form.agreed"
|
||||
class="w-5 h-5 rounded border-slate-300 text-primary-600 focus:ring-primary-500 cursor-pointer"
|
||||
/>
|
||||
<label for="agree" class="ml-3 text-slate-600 cursor-pointer">
|
||||
我已阅读并同意
|
||||
<a href="#" class="text-primary-600 hover:underline"
|
||||
>《创作者入驻协议》</a
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="pt-6">
|
||||
<button
|
||||
@click="submitForm"
|
||||
class="w-full py-4 bg-primary-600 text-white rounded-xl font-bold text-xl hover:bg-primary-700 transition-all shadow-lg disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer active:scale-95"
|
||||
:disabled="!isValid"
|
||||
>
|
||||
{{ submitting ? "正在提交..." : "提交审核" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Result -->
|
||||
<div v-else-if="step === 3" class="p-20 text-center">
|
||||
<div
|
||||
class="w-20 h-20 bg-green-100 text-green-600 rounded-full flex items-center justify-center text-4xl mx-auto mb-6 animate-in zoom-in duration-300"
|
||||
>
|
||||
<i class="pi pi-check"></i>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-slate-900 mb-4">申请已提交</h2>
|
||||
<p class="text-lg text-slate-600 mb-12 max-w-lg mx-auto">
|
||||
您的入驻申请已成功提交,平台将在 1-3
|
||||
个工作日内完成审核。审核结果将通过短信和系统通知发送给您。
|
||||
</p>
|
||||
|
||||
<div class="flex justify-center gap-4">
|
||||
<router-link
|
||||
:to="tenantRoute('/')"
|
||||
class="px-8 py-3 bg-white border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 font-medium"
|
||||
>返回首页</router-link
|
||||
>
|
||||
<router-link
|
||||
:to="tenantRoute('/me')"
|
||||
class="px-8 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 font-medium"
|
||||
>查看个人中心</router-link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,214 +1,341 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-2xl font-bold text-slate-900">管理概览</h1>
|
||||
<div class="flex gap-4">
|
||||
<router-link :to="tenantRoute('/creator/contents/new')"
|
||||
class="px-6 py-2.5 bg-primary-600 text-white rounded-lg font-bold hover:bg-primary-700 transition-colors shadow-sm shadow-primary-200 cursor-pointer active:scale-95 flex items-center gap-2">
|
||||
<i class="pi pi-plus"></i> 发布新内容
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div v-for="metric in metrics" :key="metric.label"
|
||||
class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow relative">
|
||||
<div class="text-sm text-slate-500 mb-2">{{ metric.label }}</div>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-3xl font-bold text-slate-900">{{ metric.value }}</span>
|
||||
<span class="text-xs font-bold" :class="metric.trend > 0 ? 'text-green-600' : 'text-red-600'">
|
||||
<i class="pi" :class="metric.trend > 0 ? 'pi-arrow-up' : 'pi-arrow-down'"
|
||||
style="font-size: 0.7rem"></i>
|
||||
{{ Math.abs(metric.trend) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-slate-400 mt-2">{{ metric.subtext }}</div>
|
||||
|
||||
<!-- Withdraw Button for Revenue Card -->
|
||||
<button v-if="metric.label === '累计总收益' && metric.canWithdraw" @click="showWithdraw = true"
|
||||
class="absolute top-6 right-6 px-3 py-1.5 bg-green-50 text-green-700 text-xs font-bold rounded-lg hover:bg-green-100 transition-colors">
|
||||
提现
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending Orders (Quick Access) -->
|
||||
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
||||
<h3 class="font-bold text-slate-900 mb-4">待处理事项</h3>
|
||||
<div class="flex gap-4">
|
||||
<div
|
||||
class="flex-1 p-4 bg-orange-50 border border-orange-100 rounded-xl flex items-center justify-between cursor-pointer hover:bg-orange-100 transition-colors"
|
||||
@click="$router.push(tenantRoute('/creator/orders'))">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-orange-200 text-orange-700 flex items-center justify-center"><i
|
||||
class="pi pi-refresh"></i></div>
|
||||
<div>
|
||||
<div class="font-bold text-slate-900">退款申请</div>
|
||||
<div class="text-xs text-orange-600">需要处理</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-orange-700">{{ stats.pending_refunds }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex-1 p-4 bg-blue-50 border border-blue-100 rounded-xl flex items-center justify-between cursor-pointer hover:bg-blue-100 transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-blue-200 text-blue-700 flex items-center justify-center"><i
|
||||
class="pi pi-comments"></i></div>
|
||||
<div>
|
||||
<div class="font-bold text-slate-900">新私信</div>
|
||||
<div class="text-xs text-blue-600">粉丝互动</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-blue-700">{{ stats.new_messages }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Withdraw Dialog -->
|
||||
<Dialog v-model:visible="showWithdraw" modal header="收益提现" :style="{ width: '35rem' }">
|
||||
<div class="space-y-6 pt-2">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-3">提现方式</label>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<label
|
||||
class="flex items-center gap-4 p-4 border rounded-xl cursor-pointer hover:border-primary-500 hover:bg-slate-50 transition-all"
|
||||
:class="withdrawMethod === 'external' ? 'border-primary-500 bg-primary-50 ring-1 ring-primary-500' : 'border-slate-200'">
|
||||
<input type="radio" v-model="withdrawMethod" value="external" class="hidden">
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center"><i
|
||||
class="pi pi-credit-card text-xl"></i></div>
|
||||
<div class="flex-1">
|
||||
<div class="font-bold text-slate-900">提现至银行卡/支付宝</div>
|
||||
<div class="text-xs text-slate-500" v-if="hasPayoutAccount">已绑定:{{ payoutAccounts[0].name }} ({{ payoutAccounts[0].account.slice(-4) }})</div>
|
||||
<div class="text-xs text-orange-600 font-bold flex items-center gap-1" v-else>
|
||||
<i class="pi pi-exclamation-circle"></i> 未配置收款账户
|
||||
<router-link :to="tenantRoute('/creator/settings')"
|
||||
class="underline hover:text-orange-800 ml-1">去配置</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<i v-if="withdrawMethod === 'external'" class="pi pi-check-circle text-primary-600 text-xl"></i>
|
||||
</label>
|
||||
|
||||
<label
|
||||
class="flex items-center gap-4 p-4 border rounded-xl cursor-pointer hover:border-primary-500 hover:bg-slate-50 transition-all"
|
||||
:class="withdrawMethod === 'wallet' ? 'border-primary-500 bg-primary-50 ring-1 ring-primary-500' : 'border-slate-200'">
|
||||
<input type="radio" v-model="withdrawMethod" value="wallet" class="hidden">
|
||||
<div class="w-10 h-10 rounded-full bg-green-100 text-green-600 flex items-center justify-center"><i
|
||||
class="pi pi-wallet text-xl"></i></div>
|
||||
<div class="flex-1">
|
||||
<div class="font-bold text-slate-900">转入个人钱包余额</div>
|
||||
<div class="text-xs text-slate-500">实时到账,可用于平台消费</div>
|
||||
</div>
|
||||
<i v-if="withdrawMethod === 'wallet'" class="pi pi-check-circle text-primary-600 text-xl"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">提现金额 (¥)</label>
|
||||
<input type="number" v-model="withdrawAmount" placeholder="请输入金额"
|
||||
class="w-full h-12 px-4 border border-slate-200 rounded-lg text-lg font-bold outline-none focus:border-primary-500 transition-colors">
|
||||
<div class="text-xs text-slate-400 mt-2 flex justify-between">
|
||||
<span>可提现收益: ¥ {{ stats.total_revenue.value.toLocaleString(undefined, {minimumFractionDigits: 2}) }}</span>
|
||||
<button class="text-primary-600 hover:underline" @click="withdrawAmount = stats.total_revenue.value">全部提现</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button @click="showWithdraw = false"
|
||||
class="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm font-bold">取消</button>
|
||||
<button @click="handleWithdraw"
|
||||
class="px-6 py-2 bg-primary-600 text-white rounded-lg text-sm font-bold hover:bg-primary-700 shadow-sm"
|
||||
:disabled="withdrawMethod === 'external' && !hasPayoutAccount"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': withdrawMethod === 'external' && !hasPayoutAccount }">确认提现</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Toast />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
import Dialog from "primevue/dialog";
|
||||
import Toast from "primevue/toast";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { creatorApi } from "../../api/creator";
|
||||
import { tenantPath } from "../../utils/tenant";
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const toast = useToast();
|
||||
const showWithdraw = ref(false);
|
||||
const withdrawMethod = ref('wallet');
|
||||
const withdrawAmount = ref('');
|
||||
const withdrawMethod = ref("wallet");
|
||||
const withdrawAmount = ref("");
|
||||
const payoutAccounts = ref([]);
|
||||
const stats = ref({
|
||||
total_followers: { value: 0, trend: 0 },
|
||||
total_revenue: { value: 0, trend: 0 },
|
||||
pending_refunds: 0,
|
||||
new_messages: 0
|
||||
total_followers: { value: 0, trend: 0 },
|
||||
total_revenue: { value: 0, trend: 0 },
|
||||
pending_refunds: 0,
|
||||
new_messages: 0,
|
||||
});
|
||||
|
||||
const hasPayoutAccount = computed(() => payoutAccounts.value.length > 0);
|
||||
|
||||
const metrics = computed(() => [
|
||||
{
|
||||
label: '总关注用户',
|
||||
value: stats.value.total_followers.value.toLocaleString(),
|
||||
trend: stats.value.total_followers.trend,
|
||||
subtext: '昨日数据' // Backend DTO doesn't have subtext yet, using static or simplified
|
||||
},
|
||||
{
|
||||
label: '累计总收益',
|
||||
value: '¥ ' + stats.value.total_revenue.value.toLocaleString(undefined, {minimumFractionDigits: 2}),
|
||||
trend: stats.value.total_revenue.trend,
|
||||
subtext: '近30天数据',
|
||||
canWithdraw: stats.value.total_revenue.value > 0
|
||||
},
|
||||
{
|
||||
label: "总关注用户",
|
||||
value: stats.value.total_followers.value.toLocaleString(),
|
||||
trend: stats.value.total_followers.trend,
|
||||
subtext: "昨日数据", // Backend DTO doesn't have subtext yet, using static or simplified
|
||||
},
|
||||
{
|
||||
label: "累计总收益",
|
||||
value:
|
||||
"¥ " +
|
||||
stats.value.total_revenue.value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
}),
|
||||
trend: stats.value.total_revenue.trend,
|
||||
subtext: "近30天数据",
|
||||
canWithdraw: stats.value.total_revenue.value > 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [dashboardRes, accountsRes] = await Promise.all([
|
||||
creatorApi.getDashboard(),
|
||||
creatorApi.listPayoutAccounts()
|
||||
]);
|
||||
if (dashboardRes) stats.value = dashboardRes;
|
||||
if (accountsRes) payoutAccounts.value = accountsRes;
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch creator dashboard data:", e);
|
||||
}
|
||||
try {
|
||||
const [dashboardRes, accountsRes] = await Promise.all([
|
||||
creatorApi.getDashboard(),
|
||||
creatorApi.listPayoutAccounts(),
|
||||
]);
|
||||
if (dashboardRes) stats.value = dashboardRes;
|
||||
if (accountsRes) payoutAccounts.value = accountsRes;
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch creator dashboard data:", e);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
fetchData();
|
||||
});
|
||||
|
||||
const handleWithdraw = async () => {
|
||||
if (!withdrawAmount.value || withdrawAmount.value <= 0) {
|
||||
toast.add({ severity: 'warn', summary: '提示', detail: '请输入有效的提现金额', life: 3000 });
|
||||
return;
|
||||
}
|
||||
if (!withdrawAmount.value || withdrawAmount.value <= 0) {
|
||||
toast.add({
|
||||
severity: "warn",
|
||||
summary: "提示",
|
||||
detail: "请输入有效的提现金额",
|
||||
life: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const accountId = withdrawMethod.value === 'external' ? payoutAccounts.value[0]?.id : ''; // Default to first account for now if external
|
||||
|
||||
await creatorApi.withdraw({
|
||||
amount: parseFloat(withdrawAmount.value),
|
||||
method: withdrawMethod.value,
|
||||
account_id: accountId
|
||||
});
|
||||
try {
|
||||
const accountId =
|
||||
withdrawMethod.value === "external" ? payoutAccounts.value[0]?.id : ""; // Default to first account for now if external
|
||||
|
||||
showWithdraw.value = false;
|
||||
toast.add({ severity: 'success', summary: '提现成功', detail: '提现申请已提交', life: 3000 });
|
||||
withdrawAmount.value = ''; // Reset
|
||||
fetchData(); // Refresh balance/stats
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: '提现失败', detail: '请稍后重试', life: 3000 });
|
||||
}
|
||||
await creatorApi.withdraw({
|
||||
amount: parseFloat(withdrawAmount.value),
|
||||
method: withdrawMethod.value,
|
||||
account_id: accountId,
|
||||
});
|
||||
|
||||
showWithdraw.value = false;
|
||||
toast.add({
|
||||
severity: "success",
|
||||
summary: "提现成功",
|
||||
detail: "提现申请已提交",
|
||||
life: 3000,
|
||||
});
|
||||
withdrawAmount.value = ""; // Reset
|
||||
fetchData(); // Refresh balance/stats
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "提现失败",
|
||||
detail: "请稍后重试",
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-2xl font-bold text-slate-900">管理概览</h1>
|
||||
<div class="flex gap-4">
|
||||
<router-link
|
||||
:to="tenantRoute('/creator/contents/new')"
|
||||
class="px-6 py-2.5 bg-primary-600 text-white rounded-lg font-bold hover:bg-primary-700 transition-colors shadow-sm shadow-primary-200 cursor-pointer active:scale-95 flex items-center gap-2"
|
||||
>
|
||||
<i class="pi pi-plus"></i> 发布新内容
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div
|
||||
v-for="metric in metrics"
|
||||
:key="metric.label"
|
||||
class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition-shadow relative"
|
||||
>
|
||||
<div class="text-sm text-slate-500 mb-2">{{ metric.label }}</div>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-3xl font-bold text-slate-900">{{
|
||||
metric.value
|
||||
}}</span>
|
||||
<span
|
||||
class="text-xs font-bold"
|
||||
:class="metric.trend > 0 ? 'text-green-600' : 'text-red-600'"
|
||||
>
|
||||
<i
|
||||
class="pi"
|
||||
:class="metric.trend > 0 ? 'pi-arrow-up' : 'pi-arrow-down'"
|
||||
style="font-size: 0.7rem"
|
||||
></i>
|
||||
{{ Math.abs(metric.trend) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-slate-400 mt-2">{{ metric.subtext }}</div>
|
||||
|
||||
<!-- Withdraw Button for Revenue Card -->
|
||||
<button
|
||||
v-if="metric.label === '累计总收益' && metric.canWithdraw"
|
||||
@click="showWithdraw = true"
|
||||
class="absolute top-6 right-6 px-3 py-1.5 bg-green-50 text-green-700 text-xs font-bold rounded-lg hover:bg-green-100 transition-colors"
|
||||
>
|
||||
提现
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending Orders (Quick Access) -->
|
||||
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
|
||||
<h3 class="font-bold text-slate-900 mb-4">待处理事项</h3>
|
||||
<div class="flex gap-4">
|
||||
<div
|
||||
class="flex-1 p-4 bg-orange-50 border border-orange-100 rounded-xl flex items-center justify-between cursor-pointer hover:bg-orange-100 transition-colors"
|
||||
@click="$router.push(tenantRoute('/creator/orders'))"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full bg-orange-200 text-orange-700 flex items-center justify-center"
|
||||
>
|
||||
<i class="pi pi-refresh"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-slate-900">退款申请</div>
|
||||
<div class="text-xs text-orange-600">需要处理</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-orange-700">
|
||||
{{ stats.pending_refunds }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex-1 p-4 bg-blue-50 border border-blue-100 rounded-xl flex items-center justify-between cursor-pointer hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-10 h-10 rounded-full bg-blue-200 text-blue-700 flex items-center justify-center"
|
||||
>
|
||||
<i class="pi pi-comments"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-slate-900">新私信</div>
|
||||
<div class="text-xs text-blue-600">粉丝互动</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-blue-700">
|
||||
{{ stats.new_messages }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Withdraw Dialog -->
|
||||
<Dialog
|
||||
v-model:visible="showWithdraw"
|
||||
modal
|
||||
header="收益提现"
|
||||
:style="{ width: '35rem' }"
|
||||
>
|
||||
<div class="space-y-6 pt-2">
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-3"
|
||||
>提现方式</label
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<label
|
||||
class="flex items-center gap-4 p-4 border rounded-xl cursor-pointer hover:border-primary-500 hover:bg-slate-50 transition-all"
|
||||
:class="
|
||||
withdrawMethod === 'external'
|
||||
? 'border-primary-500 bg-primary-50 ring-1 ring-primary-500'
|
||||
: 'border-slate-200'
|
||||
"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
v-model="withdrawMethod"
|
||||
value="external"
|
||||
class="hidden"
|
||||
/>
|
||||
<div
|
||||
class="w-10 h-10 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center"
|
||||
>
|
||||
<i class="pi pi-credit-card text-xl"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-bold text-slate-900">提现至银行卡/支付宝</div>
|
||||
<div class="text-xs text-slate-500" v-if="hasPayoutAccount">
|
||||
已绑定:{{ payoutAccounts[0].name }} ({{
|
||||
payoutAccounts[0].account.slice(-4)
|
||||
}})
|
||||
</div>
|
||||
<div
|
||||
class="text-xs text-orange-600 font-bold flex items-center gap-1"
|
||||
v-else
|
||||
>
|
||||
<i class="pi pi-exclamation-circle"></i> 未配置收款账户
|
||||
<router-link
|
||||
:to="tenantRoute('/creator/settings')"
|
||||
class="underline hover:text-orange-800 ml-1"
|
||||
>去配置</router-link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
v-if="withdrawMethod === 'external'"
|
||||
class="pi pi-check-circle text-primary-600 text-xl"
|
||||
></i>
|
||||
</label>
|
||||
|
||||
<label
|
||||
class="flex items-center gap-4 p-4 border rounded-xl cursor-pointer hover:border-primary-500 hover:bg-slate-50 transition-all"
|
||||
:class="
|
||||
withdrawMethod === 'wallet'
|
||||
? 'border-primary-500 bg-primary-50 ring-1 ring-primary-500'
|
||||
: 'border-slate-200'
|
||||
"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
v-model="withdrawMethod"
|
||||
value="wallet"
|
||||
class="hidden"
|
||||
/>
|
||||
<div
|
||||
class="w-10 h-10 rounded-full bg-green-100 text-green-600 flex items-center justify-center"
|
||||
>
|
||||
<i class="pi pi-wallet text-xl"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-bold text-slate-900">转入个人钱包余额</div>
|
||||
<div class="text-xs text-slate-500">
|
||||
实时到账,可用于平台消费
|
||||
</div>
|
||||
</div>
|
||||
<i
|
||||
v-if="withdrawMethod === 'wallet'"
|
||||
class="pi pi-check-circle text-primary-600 text-xl"
|
||||
></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2"
|
||||
>提现金额 (¥)</label
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
v-model="withdrawAmount"
|
||||
placeholder="请输入金额"
|
||||
class="w-full h-12 px-4 border border-slate-200 rounded-lg text-lg font-bold outline-none focus:border-primary-500 transition-colors"
|
||||
/>
|
||||
<div class="text-xs text-slate-400 mt-2 flex justify-between">
|
||||
<span
|
||||
>可提现收益: ¥
|
||||
{{
|
||||
stats.total_revenue.value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
})
|
||||
}}</span
|
||||
>
|
||||
<button
|
||||
class="text-primary-600 hover:underline"
|
||||
@click="withdrawAmount = stats.total_revenue.value"
|
||||
>
|
||||
全部提现
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button
|
||||
@click="showWithdraw = false"
|
||||
class="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm font-bold"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="handleWithdraw"
|
||||
class="px-6 py-2 bg-primary-600 text-white rounded-lg text-sm font-bold hover:bg-primary-700 shadow-sm"
|
||||
:disabled="withdrawMethod === 'external' && !hasPayoutAccount"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed':
|
||||
withdrawMethod === 'external' && !hasPayoutAccount,
|
||||
}"
|
||||
>
|
||||
确认提现
|
||||
</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Toast />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,205 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900 mb-8">频道设置</h1>
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- 0. Tips -->
|
||||
<div class="bg-blue-50 p-6 rounded-xl border border-blue-100 text-sm text-blue-700 flex items-start gap-4">
|
||||
<i class="pi pi-info-circle text-xl mt-0.5"></i>
|
||||
<div>
|
||||
<h4 class="font-bold mb-1">频道设置须知</h4>
|
||||
<ul class="list-disc list-inside space-y-1 opacity-90">
|
||||
<li>建议封面图尺寸 1280x320px,重点内容居中。</li>
|
||||
<li>频道名称修改后需人工审核,审核期间原名称仍可见。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. Basic Info & Visuals -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-8">
|
||||
<h2 class="text-lg font-bold text-slate-900 mb-6 pb-4 border-b border-slate-100">基本信息</h2>
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- Avatar & Name -->
|
||||
<div class="flex items-start gap-6">
|
||||
<div class="relative group cursor-pointer flex-shrink-0" @click="triggerUpload('avatar')">
|
||||
<div
|
||||
class="w-24 h-24 rounded-full border-4 border-slate-50 shadow-sm overflow-hidden bg-slate-100 relative">
|
||||
<img :src="form.avatar" class="w-full h-full object-cover">
|
||||
<div
|
||||
class="absolute inset-0 bg-black/40 flex items-center justify-center transition-opacity"
|
||||
:class="{'opacity-100': currentUploadType === 'avatar' && isUploading, 'opacity-0 group-hover:opacity-100': !(currentUploadType === 'avatar' && isUploading)}">
|
||||
<i v-if="currentUploadType === 'avatar' && isUploading" class="pi pi-spin pi-spinner text-white text-2xl"></i>
|
||||
<i v-else class="pi pi-camera text-white text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">频道名称</label>
|
||||
<input v-model="form.name" type="text"
|
||||
class="w-full h-11 px-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors"
|
||||
placeholder="给您的频道起个名字">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">一句话简介</label>
|
||||
<input v-model="form.bio" type="text"
|
||||
class="w-full h-11 px-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors"
|
||||
placeholder="介绍一下您的频道特色...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cover Image (Moved Here) -->
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-3">频道头图 (Cover)</label>
|
||||
<div
|
||||
class="aspect-[4/1] rounded-xl bg-slate-50 border-2 border-dashed border-slate-300 relative overflow-hidden group cursor-pointer hover:border-primary-400 transition-all"
|
||||
@click="triggerUpload('cover')">
|
||||
<img v-if="form.cover" :src="form.cover" class="w-full h-full object-cover">
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col items-center justify-center text-slate-400 bg-white/50 opacity-100 group-hover:bg-white/80 transition-all"
|
||||
v-else>
|
||||
<i class="pi pi-image text-3xl mb-2"></i>
|
||||
<span class="text-sm font-medium">点击上传 (建议尺寸 1280x320)</span>
|
||||
</div>
|
||||
<!-- Hover Overlay -->
|
||||
<div v-if="form.cover || (currentUploadType === 'cover' && isUploading)"
|
||||
class="absolute inset-0 bg-black/40 flex flex-col items-center justify-center transition-opacity"
|
||||
:class="{'opacity-100': currentUploadType === 'cover' && isUploading, 'opacity-0 group-hover:opacity-100': !(currentUploadType === 'cover' && isUploading)}">
|
||||
<template v-if="currentUploadType === 'cover' && isUploading">
|
||||
<i class="pi pi-spin pi-spinner text-white text-3xl mb-2"></i>
|
||||
<span class="text-white text-sm font-bold">上传中 {{ Math.round(uploadProgress) }}%</span>
|
||||
</template>
|
||||
<span v-else class="text-white font-bold"><i class="pi pi-refresh mr-2"></i>更换封面</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Intro Detail -->
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">详细介绍</label>
|
||||
<textarea v-model="form.description" rows="4"
|
||||
class="w-full p-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors resize-none"
|
||||
placeholder="详细描述您的履历、师承或频道内容规划..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="pt-6 border-t border-slate-100 flex justify-end">
|
||||
<button @click="saveSettings" :disabled="saveLoading"
|
||||
class="px-8 py-3 bg-primary-600 text-white rounded-lg font-bold hover:bg-primary-700 shadow-lg shadow-primary-200 cursor-pointer active:scale-95 flex items-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed">
|
||||
<i v-if="saveLoading" class="pi pi-spin pi-spinner"></i>
|
||||
<i v-else class="pi pi-check"></i>
|
||||
{{ saveLoading ? '保存中...' : '保存修改' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Payout Settings -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-8">
|
||||
<div class="flex items-center justify-between mb-6 pb-4 border-b border-slate-100">
|
||||
<h2 class="text-lg font-bold text-slate-900">收款账户</h2>
|
||||
<button @click="showAddAccount = true"
|
||||
v-if="payoutAccounts.length === 0"
|
||||
class="text-sm font-bold text-primary-600 hover:text-primary-700 cursor-pointer flex items-center gap-1 px-3 py-1.5 rounded hover:bg-primary-50 transition-colors">
|
||||
<i class="pi pi-plus"></i> 添加账户
|
||||
</button>
|
||||
<span v-else class="text-xs text-slate-400">仅支持一个收款账户,删除后可更换</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div v-for="acc in payoutAccounts" :key="acc.id"
|
||||
class="p-4 border border-slate-200 rounded-xl flex items-center gap-4 bg-white relative group hover:border-primary-200 transition-colors">
|
||||
<div class="w-10 h-10 rounded-full flex items-center justify-center text-xl"
|
||||
:class="acc.type === 'alipay' ? 'bg-blue-50 text-blue-600' : 'bg-red-50 text-red-600'">
|
||||
<i class="pi" :class="acc.type === 'alipay' ? 'pi-mobile' : 'pi-briefcase'"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-slate-900">{{ acc.name }}</div>
|
||||
<div class="text-sm text-slate-500">{{ acc.type === 'alipay' ? '支付宝' : '银行卡' }} ({{ acc.account.slice(-4) }})</div>
|
||||
</div>
|
||||
<button @click="removeAccount(acc.id)"
|
||||
class="absolute top-4 right-4 text-slate-300 hover:text-red-500 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity p-2"><i
|
||||
class="pi pi-trash"></i></button>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="payoutAccounts.length === 0" class="col-span-full py-8 text-center text-slate-400 bg-slate-50 rounded-xl border border-dashed border-slate-200">
|
||||
<i class="pi pi-wallet text-2xl mb-2"></i>
|
||||
<p>暂无收款账户,请添加</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="file" ref="fileInput" class="hidden" @change="handleFileChange">
|
||||
|
||||
<!-- Add Account Dialog -->
|
||||
<Dialog v-model:visible="showAddAccount" modal header="添加收款账户" :style="{ width: '25rem' }">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">账户类型</label>
|
||||
<div class="flex gap-4">
|
||||
<label
|
||||
class="flex items-center gap-2 cursor-pointer p-2 border border-transparent rounded hover:bg-slate-50">
|
||||
<RadioButton v-model="newAccount.type" value="bank" />
|
||||
<span>银行卡</span>
|
||||
</label>
|
||||
<label
|
||||
class="flex items-center gap-2 cursor-pointer p-2 border border-transparent rounded hover:bg-slate-50">
|
||||
<RadioButton v-model="newAccount.type" value="alipay" />
|
||||
<span>支付宝</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="newAccount.type === 'bank'">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">银行名称</label>
|
||||
<input type="text" v-model="newAccount.name"
|
||||
class="w-full h-10 px-3 border border-slate-200 rounded-lg outline-none focus:border-primary-500"
|
||||
placeholder="如:招商银行">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">{{ newAccount.type === 'bank' ? '银行卡号' :
|
||||
'支付宝账号' }}</label>
|
||||
<input type="text" v-model="newAccount.account"
|
||||
class="w-full h-10 px-3 border border-slate-200 rounded-lg outline-none focus:border-primary-500"
|
||||
placeholder="请输入账号">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">真实姓名</label>
|
||||
<input type="text" v-model="newAccount.realname"
|
||||
class="w-full h-10 px-3 border border-slate-200 rounded-lg outline-none focus:border-primary-500"
|
||||
placeholder="需与实名认证一致">
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button @click="showAddAccount = false"
|
||||
class="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm font-bold cursor-pointer">取消</button>
|
||||
<button @click="handleAddAccount"
|
||||
class="px-6 py-2 bg-primary-600 text-white rounded-lg text-sm font-bold hover:bg-primary-700 cursor-pointer shadow-sm">保存</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Toast />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Dialog from 'primevue/dialog';
|
||||
import RadioButton from 'primevue/radiobutton';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
import { commonApi } from '../../api/common';
|
||||
import Dialog from "primevue/dialog";
|
||||
import RadioButton from "primevue/radiobutton";
|
||||
import Toast from "primevue/toast";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { reactive, ref, onMounted } from "vue";
|
||||
import { creatorApi } from "../../api/creator";
|
||||
import { commonApi } from "../../api/common";
|
||||
|
||||
const toast = useToast();
|
||||
const fileInput = ref(null);
|
||||
const currentUploadType = ref('');
|
||||
const currentUploadType = ref("");
|
||||
const showAddAccount = ref(false);
|
||||
const payoutAccounts = ref([]);
|
||||
const saveLoading = ref(false);
|
||||
@@ -207,131 +17,499 @@ const isUploading = ref(false);
|
||||
const uploadProgress = ref(0);
|
||||
|
||||
const newAccount = reactive({
|
||||
type: 'bank',
|
||||
name: '',
|
||||
account: '',
|
||||
realname: ''
|
||||
type: "bank",
|
||||
name: "",
|
||||
account: "",
|
||||
realname: "",
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
bio: '',
|
||||
description: '',
|
||||
avatar: '',
|
||||
cover: ''
|
||||
name: "",
|
||||
bio: "",
|
||||
description: "",
|
||||
avatar: "",
|
||||
cover: "",
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [settings, accounts] = await Promise.all([
|
||||
creatorApi.getSettings(),
|
||||
creatorApi.listPayoutAccounts()
|
||||
]);
|
||||
if (settings) {
|
||||
Object.assign(form, settings);
|
||||
// Set defaults if empty
|
||||
if (!form.avatar) form.avatar = 'https://api.dicebear.com/7.x/avataaars/svg?seed=Master1';
|
||||
}
|
||||
payoutAccounts.value = accounts || [];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
try {
|
||||
const [settings, accounts] = await Promise.all([
|
||||
creatorApi.getSettings(),
|
||||
creatorApi.listPayoutAccounts(),
|
||||
]);
|
||||
if (settings) {
|
||||
Object.assign(form, settings);
|
||||
// Set defaults if empty
|
||||
if (!form.avatar)
|
||||
form.avatar = "https://api.dicebear.com/7.x/avataaars/svg?seed=Master1";
|
||||
}
|
||||
payoutAccounts.value = accounts || [];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(fetchData);
|
||||
|
||||
const triggerUpload = (type) => {
|
||||
currentUploadType.value = type;
|
||||
if (fileInput.value) {
|
||||
fileInput.value.accept = 'image/*';
|
||||
fileInput.value.click();
|
||||
}
|
||||
currentUploadType.value = type;
|
||||
if (fileInput.value) {
|
||||
fileInput.value.accept = "image/*";
|
||||
fileInput.value.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
isUploading.value = true;
|
||||
uploadProgress.value = 0;
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const task = commonApi.uploadWithProgress(file, 'image', (p) => {
|
||||
uploadProgress.value = p;
|
||||
});
|
||||
const res = await task.promise;
|
||||
console.log('Upload response:', res);
|
||||
|
||||
if (res && res.url) {
|
||||
if (currentUploadType.value === 'avatar') {
|
||||
form.avatar = res.url;
|
||||
} else {
|
||||
form.cover = res.url;
|
||||
}
|
||||
toast.add({ severity: 'success', summary: '上传成功', life: 2000 });
|
||||
} else {
|
||||
console.error('Invalid upload response:', res);
|
||||
toast.add({ severity: 'error', summary: '上传失败', detail: '服务器返回无效数据', life: 3000 });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.add({ severity: 'error', summary: '上传失败', detail: e.message, life: 3000 });
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
uploadProgress.value = 0;
|
||||
}
|
||||
event.target.value = '';
|
||||
isUploading.value = true;
|
||||
uploadProgress.value = 0;
|
||||
|
||||
try {
|
||||
const task = commonApi.uploadWithProgress(file, "image", (p) => {
|
||||
uploadProgress.value = p;
|
||||
});
|
||||
const res = await task.promise;
|
||||
console.log("Upload response:", res);
|
||||
|
||||
if (res && res.url) {
|
||||
if (currentUploadType.value === "avatar") {
|
||||
form.avatar = res.url;
|
||||
} else {
|
||||
form.cover = res.url;
|
||||
}
|
||||
toast.add({ severity: "success", summary: "上传成功", life: 2000 });
|
||||
} else {
|
||||
console.error("Invalid upload response:", res);
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "上传失败",
|
||||
detail: "服务器返回无效数据",
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "上传失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
uploadProgress.value = 0;
|
||||
}
|
||||
event.target.value = "";
|
||||
};
|
||||
|
||||
const handleAddAccount = async () => {
|
||||
if (payoutAccounts.value.length >= 1) {
|
||||
toast.add({ severity: 'error', summary: '限制', detail: '仅支持一个收款账户', life: 3000 });
|
||||
return;
|
||||
}
|
||||
if (newAccount.type === 'alipay') {
|
||||
newAccount.name = '支付宝';
|
||||
}
|
||||
if (!newAccount.name || !newAccount.account || !newAccount.realname) {
|
||||
toast.add({ severity: 'warn', summary: '提示', detail: '请填写完整信息', life: 3000 });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await creatorApi.addPayoutAccount(newAccount);
|
||||
showAddAccount.value = false;
|
||||
toast.add({ severity: 'success', summary: '添加成功', detail: '收款账户已添加', life: 3000 });
|
||||
fetchData();
|
||||
// Reset
|
||||
newAccount.name = ''; newAccount.account = ''; newAccount.realname = '';
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: '添加失败', detail: e.message, life: 3000 });
|
||||
}
|
||||
if (payoutAccounts.value.length >= 1) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "限制",
|
||||
detail: "仅支持一个收款账户",
|
||||
life: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (newAccount.type === "alipay") {
|
||||
newAccount.name = "支付宝";
|
||||
}
|
||||
if (!newAccount.name || !newAccount.account || !newAccount.realname) {
|
||||
toast.add({
|
||||
severity: "warn",
|
||||
summary: "提示",
|
||||
detail: "请填写完整信息",
|
||||
life: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await creatorApi.addPayoutAccount(newAccount);
|
||||
showAddAccount.value = false;
|
||||
toast.add({
|
||||
severity: "success",
|
||||
summary: "添加成功",
|
||||
detail: "收款账户已添加",
|
||||
life: 3000,
|
||||
});
|
||||
fetchData();
|
||||
// Reset
|
||||
newAccount.name = "";
|
||||
newAccount.account = "";
|
||||
newAccount.realname = "";
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "添加失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeAccount = async (id) => {
|
||||
if (!confirm('确定要删除此账户吗?')) return;
|
||||
try {
|
||||
await creatorApi.removePayoutAccount(id);
|
||||
toast.add({ severity: 'success', summary: '删除成功', life: 3000 });
|
||||
fetchData();
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: '删除失败', detail: e.message, life: 3000 });
|
||||
}
|
||||
if (!confirm("确定要删除此账户吗?")) return;
|
||||
try {
|
||||
await creatorApi.removePayoutAccount(id);
|
||||
toast.add({ severity: "success", summary: "删除成功", life: 3000 });
|
||||
fetchData();
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "删除失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
if (!form.name) {
|
||||
toast.add({ severity: 'error', summary: '错误', detail: '频道名称不能为空', life: 3000 });
|
||||
return;
|
||||
}
|
||||
if (saveLoading.value) return;
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
await creatorApi.updateSettings(form);
|
||||
toast.add({ severity: 'success', summary: '保存成功', detail: '设置已更新', life: 3000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: '保存失败', detail: e.message, life: 3000 });
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
if (!form.name) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "错误",
|
||||
detail: "频道名称不能为空",
|
||||
life: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (saveLoading.value) return;
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
await creatorApi.updateSettings(form);
|
||||
toast.add({
|
||||
severity: "success",
|
||||
summary: "保存成功",
|
||||
detail: "设置已更新",
|
||||
life: 3000,
|
||||
});
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "保存失败",
|
||||
detail: e.message,
|
||||
life: 3000,
|
||||
});
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900 mb-8">频道设置</h1>
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- 0. Tips -->
|
||||
<div
|
||||
class="bg-blue-50 p-6 rounded-xl border border-blue-100 text-sm text-blue-700 flex items-start gap-4"
|
||||
>
|
||||
<i class="pi pi-info-circle text-xl mt-0.5"></i>
|
||||
<div>
|
||||
<h4 class="font-bold mb-1">频道设置须知</h4>
|
||||
<ul class="list-disc list-inside space-y-1 opacity-90">
|
||||
<li>建议封面图尺寸 1280x320px,重点内容居中。</li>
|
||||
<li>频道名称修改后需人工审核,审核期间原名称仍可见。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. Basic Info & Visuals -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-8">
|
||||
<h2
|
||||
class="text-lg font-bold text-slate-900 mb-6 pb-4 border-b border-slate-100"
|
||||
>
|
||||
基本信息
|
||||
</h2>
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- Avatar & Name -->
|
||||
<div class="flex items-start gap-6">
|
||||
<div
|
||||
class="relative group cursor-pointer flex-shrink-0"
|
||||
@click="triggerUpload('avatar')"
|
||||
>
|
||||
<div
|
||||
class="w-24 h-24 rounded-full border-4 border-slate-50 shadow-sm overflow-hidden bg-slate-100 relative"
|
||||
>
|
||||
<img :src="form.avatar" class="w-full h-full object-cover" />
|
||||
<div
|
||||
class="absolute inset-0 bg-black/40 flex items-center justify-center transition-opacity"
|
||||
:class="{
|
||||
'opacity-100':
|
||||
currentUploadType === 'avatar' && isUploading,
|
||||
'opacity-0 group-hover:opacity-100': !(
|
||||
currentUploadType === 'avatar' && isUploading
|
||||
),
|
||||
}"
|
||||
>
|
||||
<i
|
||||
v-if="currentUploadType === 'avatar' && isUploading"
|
||||
class="pi pi-spin pi-spinner text-white text-2xl"
|
||||
></i>
|
||||
<i v-else class="pi pi-camera text-white text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2"
|
||||
>频道名称</label
|
||||
>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="w-full h-11 px-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors"
|
||||
placeholder="给您的频道起个名字"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2"
|
||||
>一句话简介</label
|
||||
>
|
||||
<input
|
||||
v-model="form.bio"
|
||||
type="text"
|
||||
class="w-full h-11 px-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors"
|
||||
placeholder="介绍一下您的频道特色..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cover Image (Moved Here) -->
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-3"
|
||||
>频道头图 (Cover)</label
|
||||
>
|
||||
<div
|
||||
class="aspect-[4/1] rounded-xl bg-slate-50 border-2 border-dashed border-slate-300 relative overflow-hidden group cursor-pointer hover:border-primary-400 transition-all"
|
||||
@click="triggerUpload('cover')"
|
||||
>
|
||||
<img
|
||||
v-if="form.cover"
|
||||
:src="form.cover"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col items-center justify-center text-slate-400 bg-white/50 opacity-100 group-hover:bg-white/80 transition-all"
|
||||
v-else
|
||||
>
|
||||
<i class="pi pi-image text-3xl mb-2"></i>
|
||||
<span class="text-sm font-medium"
|
||||
>点击上传 (建议尺寸 1280x320)</span
|
||||
>
|
||||
</div>
|
||||
<!-- Hover Overlay -->
|
||||
<div
|
||||
v-if="
|
||||
form.cover || (currentUploadType === 'cover' && isUploading)
|
||||
"
|
||||
class="absolute inset-0 bg-black/40 flex flex-col items-center justify-center transition-opacity"
|
||||
:class="{
|
||||
'opacity-100': currentUploadType === 'cover' && isUploading,
|
||||
'opacity-0 group-hover:opacity-100': !(
|
||||
currentUploadType === 'cover' && isUploading
|
||||
),
|
||||
}"
|
||||
>
|
||||
<template v-if="currentUploadType === 'cover' && isUploading">
|
||||
<i class="pi pi-spin pi-spinner text-white text-3xl mb-2"></i>
|
||||
<span class="text-white text-sm font-bold"
|
||||
>上传中 {{ Math.round(uploadProgress) }}%</span
|
||||
>
|
||||
</template>
|
||||
<span v-else class="text-white font-bold"
|
||||
><i class="pi pi-refresh mr-2"></i>更换封面</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Intro Detail -->
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2"
|
||||
>详细介绍</label
|
||||
>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="4"
|
||||
class="w-full p-4 border border-slate-200 rounded-lg focus:border-primary-500 outline-none transition-colors resize-none"
|
||||
placeholder="详细描述您的履历、师承或频道内容规划..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="pt-6 border-t border-slate-100 flex justify-end">
|
||||
<button
|
||||
@click="saveSettings"
|
||||
:disabled="saveLoading"
|
||||
class="px-8 py-3 bg-primary-600 text-white rounded-lg font-bold hover:bg-primary-700 shadow-lg shadow-primary-200 cursor-pointer active:scale-95 flex items-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
<i v-if="saveLoading" class="pi pi-spin pi-spinner"></i>
|
||||
<i v-else class="pi pi-check"></i>
|
||||
{{ saveLoading ? "保存中..." : "保存修改" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Payout Settings -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-8">
|
||||
<div
|
||||
class="flex items-center justify-between mb-6 pb-4 border-b border-slate-100"
|
||||
>
|
||||
<h2 class="text-lg font-bold text-slate-900">收款账户</h2>
|
||||
<button
|
||||
@click="showAddAccount = true"
|
||||
v-if="payoutAccounts.length === 0"
|
||||
class="text-sm font-bold text-primary-600 hover:text-primary-700 cursor-pointer flex items-center gap-1 px-3 py-1.5 rounded hover:bg-primary-50 transition-colors"
|
||||
>
|
||||
<i class="pi pi-plus"></i> 添加账户
|
||||
</button>
|
||||
<span v-else class="text-xs text-slate-400"
|
||||
>仅支持一个收款账户,删除后可更换</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div
|
||||
v-for="acc in payoutAccounts"
|
||||
:key="acc.id"
|
||||
class="p-4 border border-slate-200 rounded-xl flex items-center gap-4 bg-white relative group hover:border-primary-200 transition-colors"
|
||||
>
|
||||
<div
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center text-xl"
|
||||
:class="
|
||||
acc.type === 'alipay'
|
||||
? 'bg-blue-50 text-blue-600'
|
||||
: 'bg-red-50 text-red-600'
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="pi"
|
||||
:class="acc.type === 'alipay' ? 'pi-mobile' : 'pi-briefcase'"
|
||||
></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-slate-900">{{ acc.name }}</div>
|
||||
<div class="text-sm text-slate-500">
|
||||
{{ acc.type === "alipay" ? "支付宝" : "银行卡" }} ({{
|
||||
acc.account.slice(-4)
|
||||
}})
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="removeAccount(acc.id)"
|
||||
class="absolute top-4 right-4 text-slate-300 hover:text-red-500 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity p-2"
|
||||
>
|
||||
<i class="pi pi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-if="payoutAccounts.length === 0"
|
||||
class="col-span-full py-8 text-center text-slate-400 bg-slate-50 rounded-xl border border-dashed border-slate-200"
|
||||
>
|
||||
<i class="pi pi-wallet text-2xl mb-2"></i>
|
||||
<p>暂无收款账户,请添加</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
class="hidden"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
|
||||
<!-- Add Account Dialog -->
|
||||
<Dialog
|
||||
v-model:visible="showAddAccount"
|
||||
modal
|
||||
header="添加收款账户"
|
||||
:style="{ width: '25rem' }"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2"
|
||||
>账户类型</label
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<label
|
||||
class="flex items-center gap-2 cursor-pointer p-2 border border-transparent rounded hover:bg-slate-50"
|
||||
>
|
||||
<RadioButton v-model="newAccount.type" value="bank" />
|
||||
<span>银行卡</span>
|
||||
</label>
|
||||
<label
|
||||
class="flex items-center gap-2 cursor-pointer p-2 border border-transparent rounded hover:bg-slate-50"
|
||||
>
|
||||
<RadioButton v-model="newAccount.type" value="alipay" />
|
||||
<span>支付宝</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="newAccount.type === 'bank'">
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2"
|
||||
>银行名称</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="newAccount.name"
|
||||
class="w-full h-10 px-3 border border-slate-200 rounded-lg outline-none focus:border-primary-500"
|
||||
placeholder="如:招商银行"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2">{{
|
||||
newAccount.type === "bank" ? "银行卡号" : "支付宝账号"
|
||||
}}</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="newAccount.account"
|
||||
class="w-full h-10 px-3 border border-slate-200 rounded-lg outline-none focus:border-primary-500"
|
||||
placeholder="请输入账号"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-700 mb-2"
|
||||
>真实姓名</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="newAccount.realname"
|
||||
class="w-full h-10 px-3 border border-slate-200 rounded-lg outline-none focus:border-primary-500"
|
||||
placeholder="需与实名认证一致"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button
|
||||
@click="showAddAccount = false"
|
||||
class="px-4 py-2 text-slate-500 hover:text-slate-700 text-sm font-bold cursor-pointer"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="handleAddAccount"
|
||||
class="px-6 py-2 bg-primary-600 text-white rounded-lg text-sm font-bold hover:bg-primary-700 cursor-pointer shadow-sm"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Toast />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user