chore: update auth and portal

This commit is contained in:
2026-01-14 11:29:17 +08:00
parent fb0a1c2f84
commit 3bcee7efc2
42 changed files with 5969 additions and 3014 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>