homevue
This commit is contained in:
72
src/components/AppSidebar.vue
Normal file
72
src/components/AppSidebar.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ name: "home", label: "首页", to: "/", icon: "home" },
|
||||||
|
{ name: "avatar", label: "形象", to: "/avatar", icon: "avatar" },
|
||||||
|
{ name: "material", label: "素材", to: "/material", icon: "material" },
|
||||||
|
{ name: "voice", label: "声音", to: "/voice", icon: "voice" },
|
||||||
|
{ name: "compose", label: "合成", to: "/compose", icon: "compose" },
|
||||||
|
{ name: "extract", label: "提取", to: "/extract", icon: "extract" },
|
||||||
|
{ name: "design", label: "设计", to: "/design", icon: "design" },
|
||||||
|
{ name: "model", label: "模型", to: "/model", icon: "model" },
|
||||||
|
{ name: "help", label: "帮助", to: "/help", icon: "help" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const activeName = computed(() => route.name);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="sidebar-nav" aria-label="主导航">
|
||||||
|
<RouterLink
|
||||||
|
v-for="item in menuItems"
|
||||||
|
:key="item.name"
|
||||||
|
:to="item.to"
|
||||||
|
class="sidebar-item"
|
||||||
|
:class="{ 'sidebar-item--active': activeName === item.name }"
|
||||||
|
>
|
||||||
|
<span class="sidebar-icon" aria-hidden="true">
|
||||||
|
<svg v-if="item.icon === 'home'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M4 10.5 12 4l8 6.5V20a1 1 0 0 1-1 1h-5v-6H10v6H5a1 1 0 0 1-1-1v-9.5z" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="item.icon === 'avatar'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<rect x="4" y="4" width="16" height="16" rx="3" />
|
||||||
|
<path d="M10 9.5v5l4-2.5-4-2.5z" fill="currentColor" stroke="none" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="item.icon === 'material'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||||
|
<circle cx="8.5" cy="10" r="1.5" fill="currentColor" stroke="none" />
|
||||||
|
<path d="M3 16l5-4 4 3 5-5 4 4" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="item.icon === 'voice'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M12 4a3 3 0 0 1 3 3v5a3 3 0 0 1-6 0V7a3 3 0 0 1 3-3z" />
|
||||||
|
<path d="M6 11a6 6 0 0 0 12 0M12 17v3" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="item.icon === 'compose'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<rect x="3" y="6" width="18" height="12" rx="2" />
|
||||||
|
<path d="M7 6V4h10v2M10 12l2 1.5 2-1.5v-3l-2-1.5-2 1.5v3z" fill="currentColor" stroke="none" opacity="0.9" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="item.icon === 'extract'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<circle cx="6" cy="6" r="2.5" />
|
||||||
|
<circle cx="18" cy="18" r="2.5" />
|
||||||
|
<path d="M8 8l8 8M16 6l2 2M6 16l2 2" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="item.icon === 'design'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M14 4l6 6-10 10H4v-6L14 4z" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="item.icon === 'model'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M12 3l7 4v10l-7 4-7-4V7l7-4z" />
|
||||||
|
<path d="M12 3v18M5 7l7 4 7-4" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="item.icon === 'help'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<path d="M9.5 9a2.5 2.5 0 1 1 4.2 1.8c-.8.7-1.2 1.2-1.2 2.2M12 17h.01" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="sidebar-label">{{ item.label }}</span>
|
||||||
|
</RouterLink>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
48
src/components/dashboard/DashboardCard.vue
Normal file
48
src/components/dashboard/DashboardCard.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
defineOptions({ name: "DashboardCard" });
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
step: { type: String, default: "" },
|
||||||
|
defaultOpen: { type: Boolean, default: true },
|
||||||
|
grow: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const open = ref(props.defaultOpen);
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
open.value = !open.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="dashboard-card"
|
||||||
|
:class="{ 'dashboard-card--grow': props.grow }"
|
||||||
|
>
|
||||||
|
<header class="dashboard-card-header cursor-pointer" @click="toggle">
|
||||||
|
<div class="flex min-w-0 items-center gap-2">
|
||||||
|
<span v-if="props.step" class="step-badge">{{ props.step }}</span>
|
||||||
|
<h3 class="dashboard-card-title truncate">{{ props.title }}</h3>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="dashboard-chevron shrink-0"
|
||||||
|
:class="{ 'dashboard-chevron--open': open }"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M39.6 17.443 24.043 33 8.487 17.443" />
|
||||||
|
</svg>
|
||||||
|
</header>
|
||||||
|
<div v-show="open" class="dashboard-card-body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
9
src/components/dashboard/StepBadge.vue
Normal file
9
src/components/dashboard/StepBadge.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
step: { type: String, required: true },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="step-badge">{{ step }}</span>
|
||||||
|
</template>
|
||||||
12
src/layouts/MainLayout.vue
Normal file
12
src/layouts/MainLayout.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup>
|
||||||
|
import AppSidebar from "../components/AppSidebar.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full min-h-0 bg-bg-deep">
|
||||||
|
<AppSidebar />
|
||||||
|
<div class="main-content min-w-0 flex-1 overflow-hidden">
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
24
src/main.js
24
src/main.js
@@ -7,7 +7,19 @@ import InputText from "primevue/inputtext";
|
|||||||
import Password from "primevue/password";
|
import Password from "primevue/password";
|
||||||
import Message from "primevue/message";
|
import Message from "primevue/message";
|
||||||
import Card from "primevue/card";
|
import Card from "primevue/card";
|
||||||
|
import Select from "primevue/select";
|
||||||
|
import Textarea from "primevue/textarea";
|
||||||
|
import InputNumber from "primevue/inputnumber";
|
||||||
|
import Slider from "primevue/slider";
|
||||||
|
import Checkbox from "primevue/checkbox";
|
||||||
|
import SelectButton from "primevue/selectbutton";
|
||||||
|
import Tabs from "primevue/tabs";
|
||||||
|
import TabList from "primevue/tablist";
|
||||||
|
import Tab from "primevue/tab";
|
||||||
|
import TabPanels from "primevue/tabpanels";
|
||||||
|
import TabPanel from "primevue/tabpanel";
|
||||||
|
|
||||||
|
import DashboardCard from "./components/dashboard/DashboardCard.vue";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import "./styles/main.css";
|
import "./styles/main.css";
|
||||||
@@ -31,5 +43,17 @@ app.component("InputText", InputText);
|
|||||||
app.component("Password", Password);
|
app.component("Password", Password);
|
||||||
app.component("Message", Message);
|
app.component("Message", Message);
|
||||||
app.component("Card", Card);
|
app.component("Card", Card);
|
||||||
|
app.component("Select", Select);
|
||||||
|
app.component("Textarea", Textarea);
|
||||||
|
app.component("InputNumber", InputNumber);
|
||||||
|
app.component("Slider", Slider);
|
||||||
|
app.component("Checkbox", Checkbox);
|
||||||
|
app.component("SelectButton", SelectButton);
|
||||||
|
app.component("Tabs", Tabs);
|
||||||
|
app.component("TabList", TabList);
|
||||||
|
app.component("Tab", Tab);
|
||||||
|
app.component("TabPanels", TabPanels);
|
||||||
|
app.component("TabPanel", TabPanel);
|
||||||
|
app.component("DashboardCard", DashboardCard);
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
|||||||
@@ -1,12 +1,68 @@
|
|||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import { useAuthStore } from "../stores/auth";
|
import { useAuthStore } from "../stores/auth";
|
||||||
|
|
||||||
|
const placeholder = () => import("../views/PlaceholderView.vue");
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "home",
|
component: () => import("../layouts/MainLayout.vue"),
|
||||||
component: () => import("../views/HomeView.vue"),
|
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
name: "home",
|
||||||
|
component: () => import("../views/HomeView.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "avatar",
|
||||||
|
name: "avatar",
|
||||||
|
component: placeholder,
|
||||||
|
meta: { title: "形象" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "material",
|
||||||
|
name: "material",
|
||||||
|
component: placeholder,
|
||||||
|
meta: { title: "素材" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "voice",
|
||||||
|
name: "voice",
|
||||||
|
component: placeholder,
|
||||||
|
meta: { title: "声音" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "compose",
|
||||||
|
name: "compose",
|
||||||
|
component: placeholder,
|
||||||
|
meta: { title: "合成" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "extract",
|
||||||
|
name: "extract",
|
||||||
|
component: placeholder,
|
||||||
|
meta: { title: "提取" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "design",
|
||||||
|
name: "design",
|
||||||
|
component: placeholder,
|
||||||
|
meta: { title: "设计" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "model",
|
||||||
|
name: "model",
|
||||||
|
component: placeholder,
|
||||||
|
meta: { title: "模型" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "help",
|
||||||
|
name: "help",
|
||||||
|
component: placeholder,
|
||||||
|
meta: { title: "帮助" },
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
@@ -31,8 +87,9 @@ const router = createRouter({
|
|||||||
|
|
||||||
router.beforeEach((to) => {
|
router.beforeEach((to) => {
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
|
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
|
||||||
|
|
||||||
if (to.meta.requiresAuth && !auth.isAuthenticated) {
|
if (requiresAuth && !auth.isAuthenticated) {
|
||||||
return { name: "login" };
|
return { name: "login" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,3 +102,254 @@ body {
|
|||||||
.auth-link:hover {
|
.auth-link:hover {
|
||||||
color: var(--color-accent-cyan);
|
color: var(--color-accent-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 72px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 12px 0;
|
||||||
|
gap: 4px;
|
||||||
|
background: #0a0c14;
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
gap: 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
color 0.2s,
|
||||||
|
background 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item:hover {
|
||||||
|
color: #e2e8f0;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-item--active {
|
||||||
|
color: #e2e8f0;
|
||||||
|
border: 1px solid rgba(0, 229, 255, 0.45);
|
||||||
|
background: rgba(0, 229, 255, 0.06);
|
||||||
|
box-shadow: 0 0 12px rgba(0, 229, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-label {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 60% 40% at 50% 0%, rgba(0, 229, 255, 0.06), transparent),
|
||||||
|
var(--color-bg-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 首页工作台 */
|
||||||
|
.custom-scrollbar {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0, 229, 255, 0.25) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 229, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-page {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card {
|
||||||
|
background: #141414;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.25s, border-color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card:hover {
|
||||||
|
border-color: rgba(0, 229, 255, 0.2);
|
||||||
|
box-shadow: 0 0 16px rgba(0, 229, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card--grow {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card--grow .dashboard-card-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-chevron {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: var(--color-accent-cyan-dim);
|
||||||
|
transition: transform 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-chevron--open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-field-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-field-label--muted {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-muted {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-preview-box {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-aspect-video {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-platform-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-platform-row:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-one-click {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(0, 229, 255, 0.35);
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(0, 229, 255, 0.12) 0%,
|
||||||
|
rgba(99, 102, 241, 0.15) 100%
|
||||||
|
);
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-one-click:hover {
|
||||||
|
border-color: rgba(0, 229, 255, 0.55);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 229, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-textarea-mono :deep(textarea) {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,465 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useRouter } from "vue-router";
|
import { ref } from "vue";
|
||||||
import { useAuthStore } from "../stores/auth";
|
const ipMode = ref("ipBrain");
|
||||||
|
const ipModes = [
|
||||||
|
{ label: "IP学习", value: "ipBrain" },
|
||||||
|
{ label: "视频学习", value: "videoWrite" },
|
||||||
|
{ label: "爆款文案", value: "customPrompt" },
|
||||||
|
];
|
||||||
|
|
||||||
const router = useRouter();
|
const videoType = ref("口播文案");
|
||||||
const auth = useAuthStore();
|
const videoTypes = ["口播文案", "剧情文案", "带货文案"];
|
||||||
|
const copyType = ref("人设型");
|
||||||
|
const copyTypes = ["人设型", "卖点型", "故事型"];
|
||||||
|
|
||||||
async function onLogout() {
|
const scriptContent = ref("");
|
||||||
await auth.logout();
|
const industryPersona = ref("");
|
||||||
router.push({ name: "login" });
|
const productBusiness = ref("");
|
||||||
}
|
const sellingPrice = ref("");
|
||||||
|
const otherRequirements = ref("");
|
||||||
|
const targetWords = ref(300);
|
||||||
|
|
||||||
|
const voiceEmotion = ref("自然");
|
||||||
|
const voiceEmotions = ["自然", "热情", "沉稳"];
|
||||||
|
const speechRate = ref(1);
|
||||||
|
const voiceLanguage = ref("中文(普通话)");
|
||||||
|
const languages = ["中文(普通话)", "中文(粤语)", "English"];
|
||||||
|
const avatarSelect = ref(null);
|
||||||
|
const avatarOptions = [{ label: "请选择形象", value: null }];
|
||||||
|
|
||||||
|
const editLanguage = ref("中文(普通话)");
|
||||||
|
const editWordCount = ref(300);
|
||||||
|
|
||||||
|
const titleGenerated = ref("");
|
||||||
|
const tagsGenerated = ref("");
|
||||||
|
const keywordTab = ref("0");
|
||||||
|
const keywordsFocus = ref("");
|
||||||
|
|
||||||
|
const autoSubtitle = ref(false);
|
||||||
|
const smartSubtitle = ref(true);
|
||||||
|
const bgmEnabled = ref(false);
|
||||||
|
const bgmVolume = ref(30);
|
||||||
|
|
||||||
|
const pipInPicture = ref(false);
|
||||||
|
const autoCutBreath = ref(false);
|
||||||
|
const greenScreen = ref(false);
|
||||||
|
|
||||||
|
const publishPlatforms = ref([
|
||||||
|
{ label: "*音", checked: false, account: null },
|
||||||
|
{ label: "*手", checked: false, account: null },
|
||||||
|
{ label: "*频号", checked: false, account: null },
|
||||||
|
{ label: "*红书", checked: false, account: null },
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="dashboard-page custom-scrollbar">
|
||||||
class="flex h-full flex-col items-center justify-center gap-6 bg-bg-base p-8"
|
<div class="dashboard-grid">
|
||||||
style="
|
<!-- 第 1 列 -->
|
||||||
background:
|
<div class="dashboard-col">
|
||||||
radial-gradient(ellipse 60% 40% at 50% 0%, rgba(0, 229, 255, 0.08), transparent),
|
<DashboardCard title="IP深度学习" step="01">
|
||||||
var(--color-bg-base);
|
<SelectButton
|
||||||
"
|
v-model="ipMode"
|
||||||
>
|
:options="ipModes"
|
||||||
<div class="text-center">
|
option-label="label"
|
||||||
<p class="font-mono text-xs tracking-widest text-text-muted uppercase">
|
option-value="value"
|
||||||
已连接
|
class="mb-3 w-full"
|
||||||
</p>
|
size="small"
|
||||||
<h1 class="gradient-text mt-2 text-2xl font-bold">
|
/>
|
||||||
欢迎,{{ auth.currentUser?.username }}
|
|
||||||
</h1>
|
|
||||||
<p class="mt-3 max-w-sm text-sm text-text-muted">
|
|
||||||
主界面开发中。当前为登录成功占位页。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button label="退出登录" severity="secondary" outlined @click="onLogout" />
|
<template v-if="ipMode === 'ipBrain'">
|
||||||
|
<Button label="添加IP大脑" class="w-full" size="small" />
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="dashboard-field-label">
|
||||||
|
已学习到的对标
|
||||||
|
<span class="dashboard-field-label--muted text-xs">(0/5)</span>
|
||||||
|
</div>
|
||||||
|
<p class="dashboard-muted py-2">暂无对标</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="dashboard-field-label mb-2">选题库</div>
|
||||||
|
<p class="dashboard-muted py-2 text-center text-sm">请先选择一个对标</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="ipMode === 'videoWrite'">
|
||||||
|
<Button label="打开视频链接" class="w-full" size="small" />
|
||||||
|
<div class="mt-3 space-y-1">
|
||||||
|
<div class="dashboard-field-label flex items-center gap-1">
|
||||||
|
<span>识别的原始内容</span>
|
||||||
|
<span class="text-xs font-normal text-slate-500">(使用视频仿写功能后显示)</span>
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
readonly
|
||||||
|
placeholder="使用【打开视频链接】功能后,识别的原始内容将显示在这里..."
|
||||||
|
class="dashboard-textarea-mono w-full"
|
||||||
|
rows="10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="space-y-2.5">
|
||||||
|
<div>
|
||||||
|
<div class="dashboard-field-label">
|
||||||
|
视频类型 <span class="text-xs text-red-400">*</span>
|
||||||
|
</div>
|
||||||
|
<Select v-model="videoType" :options="videoTypes" size="small" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="dashboard-field-label">
|
||||||
|
文案类型 <span class="text-xs text-red-400">*</span>
|
||||||
|
</div>
|
||||||
|
<Select v-model="copyType" :options="copyTypes" size="small" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="dashboard-field-label">
|
||||||
|
行业+人设 <span class="text-xs text-slate-400">(可选)</span>
|
||||||
|
</div>
|
||||||
|
<InputText
|
||||||
|
v-model="industryPersona"
|
||||||
|
placeholder="例如:餐饮店,我叫斌哥,在上海,有10年餐饮经验"
|
||||||
|
size="small"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="dashboard-field-label">
|
||||||
|
产品/业务 <span class="text-xs text-slate-400">(可选)</span>
|
||||||
|
</div>
|
||||||
|
<InputText
|
||||||
|
v-model="productBusiness"
|
||||||
|
placeholder="例如:纸巾,麻辣烫,房产,教培,财务..."
|
||||||
|
size="small"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="dashboard-field-label">
|
||||||
|
卖点+价格 <span class="text-xs text-slate-400">(可选)</span>
|
||||||
|
</div>
|
||||||
|
<InputText
|
||||||
|
v-model="sellingPrice"
|
||||||
|
placeholder="例如:纸张柔软亲肤,正常价99,今天只要59元"
|
||||||
|
size="small"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="dashboard-field-label">
|
||||||
|
其他要求 <span class="text-xs text-slate-400">(可选)</span>
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
v-model="otherRequirements"
|
||||||
|
placeholder="例如:风格幽默,突出性价比,适合30-50岁人群..."
|
||||||
|
rows="3"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="dashboard-field-label">目标字数</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Slider v-model="targetWords" :min="50" :max="1500" class="flex-1" />
|
||||||
|
<InputNumber
|
||||||
|
v-model="targetWords"
|
||||||
|
:min="50"
|
||||||
|
:max="1500"
|
||||||
|
size="small"
|
||||||
|
class="w-24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button label="生成爆款文案" size="small" class="w-full" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DashboardCard>
|
||||||
|
|
||||||
|
<DashboardCard title="视频文案编辑" :grow="true">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="dashboard-field-label">文案内容</div>
|
||||||
|
<Textarea
|
||||||
|
v-model="scriptContent"
|
||||||
|
placeholder="文案内容将显示在这里..."
|
||||||
|
class="dashboard-textarea-mono w-full"
|
||||||
|
rows="12"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 pt-1">
|
||||||
|
<InputNumber
|
||||||
|
v-model="editWordCount"
|
||||||
|
:min="100"
|
||||||
|
:max="2000"
|
||||||
|
size="small"
|
||||||
|
class="w-28"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-slate-400">字</span>
|
||||||
|
<Select
|
||||||
|
v-model="editLanguage"
|
||||||
|
:options="languages"
|
||||||
|
size="small"
|
||||||
|
class="w-36"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 pt-1">
|
||||||
|
<Button label="撰写文案" size="small" />
|
||||||
|
<Button label="AI法务" size="small" severity="secondary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashboardCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第 2 列 -->
|
||||||
|
<div class="dashboard-col">
|
||||||
|
<DashboardCard title="音视频生成" step="02">
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="mb-1 text-xs text-slate-200">音色</div>
|
||||||
|
<Button
|
||||||
|
label="选择音色"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
outlined
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-1 text-xs text-slate-200">情绪</div>
|
||||||
|
<Select
|
||||||
|
v-model="voiceEmotion"
|
||||||
|
:options="voiceEmotions"
|
||||||
|
size="small"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-1 text-xs text-slate-200">语速</div>
|
||||||
|
<InputNumber
|
||||||
|
v-model="speechRate"
|
||||||
|
:min="0.5"
|
||||||
|
:max="2"
|
||||||
|
:step="0.1"
|
||||||
|
size="small"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex items-end gap-2">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="mb-1 text-xs text-slate-200">语言</div>
|
||||||
|
<Select
|
||||||
|
v-model="voiceLanguage"
|
||||||
|
:options="languages"
|
||||||
|
size="small"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button label="生成语音" size="small" class="shrink-0" />
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-preview-box mt-3">
|
||||||
|
<div class="mb-2 flex items-center justify-between gap-2">
|
||||||
|
<span class="text-xs text-slate-400">音频预览</span>
|
||||||
|
<Select placeholder="暂无历史记录" disabled size="small" class="w-40" />
|
||||||
|
</div>
|
||||||
|
<p class="py-4 text-center text-xs text-gray-500">
|
||||||
|
暂无音频,请生成或选择历史记录
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="mb-2 text-xs text-slate-400">选择形象</div>
|
||||||
|
<Select
|
||||||
|
v-model="avatarSelect"
|
||||||
|
:options="avatarOptions"
|
||||||
|
option-label="label"
|
||||||
|
option-value="value"
|
||||||
|
placeholder="请选择形象"
|
||||||
|
size="small"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button label="生成口播视频" size="small" class="mt-3 w-full" />
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="mb-1 text-xs text-slate-200">视频预览</div>
|
||||||
|
<div class="dashboard-aspect-video">
|
||||||
|
<span class="text-xs text-gray-400">暂无视频预览</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashboardCard>
|
||||||
|
|
||||||
|
<DashboardCard title="视频编辑" step="03" :grow="true">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="flex items-center gap-2 text-sm text-slate-200">
|
||||||
|
<Checkbox v-model="pipInPicture" binary />
|
||||||
|
画中画
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 text-sm text-slate-200">
|
||||||
|
<Checkbox v-model="autoCutBreath" binary />
|
||||||
|
自动剪气口
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 text-sm text-slate-200">
|
||||||
|
<Checkbox v-model="greenScreen" binary />
|
||||||
|
启动绿幕切换
|
||||||
|
</label>
|
||||||
|
<Button label="自动处理" size="small" class="w-full" disabled />
|
||||||
|
</div>
|
||||||
|
</DashboardCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第 3 列 -->
|
||||||
|
<div class="dashboard-col">
|
||||||
|
<DashboardCard title="标题标签关键词" step="04">
|
||||||
|
<Button label="生成标题标签关键词" size="small" class="mb-3 w-full" />
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="dashboard-field-label mb-1">生成的标题(可编辑)</div>
|
||||||
|
<Textarea
|
||||||
|
v-model="titleGenerated"
|
||||||
|
placeholder="标题将显示在这里..."
|
||||||
|
rows="2"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="dashboard-field-label mb-1">生成的标签(可编辑)</div>
|
||||||
|
<Textarea
|
||||||
|
v-model="tagsGenerated"
|
||||||
|
placeholder="标签将显示在这里,逗号分隔..."
|
||||||
|
rows="2"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="dashboard-field-label mb-2">生成的关键词</div>
|
||||||
|
<Tabs v-model:value="keywordTab">
|
||||||
|
<TabList>
|
||||||
|
<Tab value="0">重点词/成语词</Tab>
|
||||||
|
<Tab value="1">描述词</Tab>
|
||||||
|
<Tab value="2">行动词</Tab>
|
||||||
|
<Tab value="3">情感词</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel value="0">
|
||||||
|
<Textarea
|
||||||
|
v-model="keywordsFocus"
|
||||||
|
placeholder="请输入重点词/成语词,每行一个..."
|
||||||
|
rows="8"
|
||||||
|
class="dashboard-textarea-mono w-full"
|
||||||
|
/>
|
||||||
|
<p class="dashboard-muted mt-2 text-sm">已添加 0 个关键词</p>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value="1">
|
||||||
|
<Textarea
|
||||||
|
placeholder="请输入描述词,每行一个..."
|
||||||
|
rows="8"
|
||||||
|
class="dashboard-textarea-mono w-full"
|
||||||
|
/>
|
||||||
|
<p class="dashboard-muted mt-2 text-sm">已添加 0 个关键词</p>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value="2">
|
||||||
|
<Textarea
|
||||||
|
placeholder="请输入行动词,每行一个..."
|
||||||
|
rows="8"
|
||||||
|
class="dashboard-textarea-mono w-full"
|
||||||
|
/>
|
||||||
|
<p class="dashboard-muted mt-2 text-sm">已添加 0 个关键词</p>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value="3">
|
||||||
|
<Textarea
|
||||||
|
placeholder="请输入情感词,每行一个..."
|
||||||
|
rows="8"
|
||||||
|
class="dashboard-textarea-mono w-full"
|
||||||
|
/>
|
||||||
|
<p class="dashboard-muted mt-2 text-sm">已添加 0 个关键词</p>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</DashboardCard>
|
||||||
|
|
||||||
|
<DashboardCard title="字幕和音乐" step="05" :grow="true">
|
||||||
|
<label class="flex items-center gap-2 text-sm">
|
||||||
|
<Checkbox v-model="autoSubtitle" binary />
|
||||||
|
自动生成字幕
|
||||||
|
</label>
|
||||||
|
<label class="mt-2 flex items-center gap-2 text-sm">
|
||||||
|
<Checkbox v-model="smartSubtitle" binary />
|
||||||
|
启用智能字幕
|
||||||
|
</label>
|
||||||
|
<div class="mt-2 flex items-center gap-2">
|
||||||
|
<Button label="模板选择" size="small" outlined />
|
||||||
|
<span class="dashboard-muted truncate text-sm">已选模板: 未选择</span>
|
||||||
|
</div>
|
||||||
|
<label class="mt-3 flex items-center gap-2 text-sm">
|
||||||
|
<Checkbox v-model="bgmEnabled" binary />
|
||||||
|
添加背景音乐
|
||||||
|
</label>
|
||||||
|
<div class="mt-2 flex flex-wrap items-center gap-2">
|
||||||
|
<InputNumber
|
||||||
|
v-model="bgmVolume"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:disabled="!bgmEnabled"
|
||||||
|
size="small"
|
||||||
|
class="w-20"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-slate-400">%</span>
|
||||||
|
<Button label="选择音乐" size="small" severity="secondary" :disabled="!bgmEnabled" />
|
||||||
|
<Button label="试听" size="small" outlined :disabled="!bgmEnabled" />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
label="自动生成字幕和BGM"
|
||||||
|
size="small"
|
||||||
|
class="mt-3 w-full"
|
||||||
|
:disabled="!autoSubtitle && !bgmEnabled"
|
||||||
|
/>
|
||||||
|
</DashboardCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第 4 列 -->
|
||||||
|
<div class="dashboard-col">
|
||||||
|
<section class="dashboard-card p-3">
|
||||||
|
<button type="button" class="dashboard-one-click">开启一键自动</button>
|
||||||
|
<div class="mt-2 grid grid-cols-2 gap-2">
|
||||||
|
<Button label="停止任务" size="small" text severity="danger" />
|
||||||
|
<Button label="清除数据" size="small" text severity="danger" />
|
||||||
|
</div>
|
||||||
|
<Button label="智能体配置" size="small" outlined class="mt-2 w-full" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<DashboardCard title="封面制作" step="06">
|
||||||
|
<Button label="自动生成封面" size="small" class="w-full" />
|
||||||
|
<Button label="封面设置" size="small" severity="secondary" class="mt-2 w-full" />
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="dashboard-field-label mb-2">封面预览</div>
|
||||||
|
<div class="dashboard-aspect-video">
|
||||||
|
<span class="dashboard-muted text-sm">暂无封面预览</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DashboardCard>
|
||||||
|
|
||||||
|
<DashboardCard title="视频发布" step="07" :grow="true">
|
||||||
|
<div
|
||||||
|
v-for="(platform, index) in publishPlatforms"
|
||||||
|
:key="index"
|
||||||
|
class="dashboard-platform-row mb-2"
|
||||||
|
>
|
||||||
|
<label class="flex shrink-0 items-center gap-2 text-sm" style="min-width: 70px">
|
||||||
|
<Checkbox v-model="platform.checked" binary />
|
||||||
|
{{ platform.label }}
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
v-model="platform.account"
|
||||||
|
placeholder="选择账号"
|
||||||
|
size="small"
|
||||||
|
class="min-w-0 flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex gap-2">
|
||||||
|
<Button label="发布" size="small" class="flex-1" />
|
||||||
|
<Button label="定时发布" size="small" class="flex-1" />
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-center text-xs leading-relaxed text-gray-400 opacity-80">
|
||||||
|
首次发布:请手动关闭浏览器内出现的任何弹窗提示(说明等),防止干扰脚本,下次即可流畅运行
|
||||||
|
</p>
|
||||||
|
</DashboardCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
14
src/views/PlaceholderView.vue
Normal file
14
src/views/PlaceholderView.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const title = computed(() => route.meta?.title || "页面");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full flex-col items-center justify-center gap-3 p-8">
|
||||||
|
<h1 class="gradient-text text-xl font-semibold">{{ title }}</h1>
|
||||||
|
<p class="text-sm text-text-muted">功能开发中</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user