Compare commits

...

4 Commits

Author SHA1 Message Date
9ebb7e5452 feat: order page 2025-12-26 09:34:41 +08:00
15a5d94e83 fix: drop down menu issues 2025-12-26 09:21:58 +08:00
bcbd3327ea feat: add initial styling and layout for portal views
- Created a global CSS file for consistent styling across the application.
- Implemented the Explore, Home, Topics, and various user views with responsive design.
- Added a Login view with OTP functionality and a Checkout view for order processing.
- Developed a NotFound view for handling 404 errors.
- Established a configuration file for Vite with Tailwind CSS integration.
2025-12-26 09:18:41 +08:00
a4262b4a52 feat: 更新租户申请和管理页面的路由及布局信息 2025-12-25 23:57:40 +08:00
45 changed files with 4183 additions and 831 deletions

View File

@@ -1,7 +1,6 @@
# 页面设计: 租户申请/成为创作者 (Tenant Application)
> **路由**: `/me/tenant/apply`
> **布局**: `LayoutMain` (头部增加引导展示)
> **路由**: `/creator/apply` > **布局**: `LayoutMain` (头部增加引导展示)
> **入口**: `TopNavbar` 右侧 [成为创作者] 按钮 (仅对非租户显示)
## 1. 流程结构 (Flow)
@@ -17,16 +16,19 @@
## 2. 详细设计 (Detailed Specs)
### 2.1 入驻引导页 (Landing)
用户首次点击 [成为创作者] 时展示。
- **视觉 (Hero)**:
- **视觉 (Hero)**:
- 简洁、大气的插画/背景,文案: "开启您的内容创作之旅"。
- 核心权益 (3个大图标):
- 核心权益 (3 个大图标):
1. **内容变现**: 设置付费阅读,直接获得收益。
2. **租户主页**: 拥有独立的品牌/个人展示空间。
3. **数据管理**: 清晰的内容与订单统计。
- **操作**: 底部显著的 [立即申请] 大按钮。
### 2.2 申请表单页 (Application Form)
**容器**: `max-w-3xl mx-auto bg-white rounded-lg shadow-sm p-10 my-8`
- **表单字段 (适老化大输入框)**:
@@ -34,14 +36,15 @@
2. **租户头像**: (必填) 品牌或个人头像。
3. **内容领域**: (单选) 如科技、生活、养老、医疗等。
4. **简介/Slogan**: (必填) 一句话介绍。
5. **资质材料 (可选/必填)**:
- 针对个人:身份证件照片。
- 针对机构:营业执照/相关资质。
- **交互**: 图片上传组件,带大面积点击区和 [查看范例] 链接。
5. **资质材料 (可选/必填)**:
- 针对个人:身份证件照片。
- 针对机构:营业执照/相关资质。
- **交互**: 图片上传组件,带大面积点击区和 [查看范例] 链接。
- **协议勾选**: `[ ] 我已阅读并同意《创作者入驻协议》` (新窗口打开)。
- **操作**: [提交审核] (Primary Button, h-14, text-xl)。
### 2.3 审核结果页 (Review Status)
- **提交成功**:
- 绿色对勾图标 + "申请已提交"。
- 文案: "平台将在 1-3 个工作日内完成审核,结果将通过系统通知告知您。"。
@@ -55,7 +58,7 @@
## 3. 交互与权限
- **按钮显隐**:
- **按钮显隐**:
- 已有租户身份的用户,[成为创作者] 按钮替换为 [进入管理后台]。
- 游客点击 [成为创作者] 按钮,弹出登录提示。
- **状态持久化**:

View File

@@ -1,7 +1,6 @@
# 页面设计: 租户/个人主页 (Tenant Profile)
> **路由**: `/tenants/:tenantId`
> **布局**: `LayoutMain` (Header + Footer)
> **路由**: `/t/:tenantId` > **布局**: `LayoutMain` (Header + Footer)
> **结构**: 固定宽度 (1280px) 封面头部 + 切换内容区
## 1. 页面结构 (Structure)
@@ -25,7 +24,8 @@
## 2. 详细设计 (Detailed Specs)
### 2.1 头部区域 (Profile Header)
- **封面图 (Cover)**:
- **封面图 (Cover)**:
- 尺寸: **1280x320px** (4:1 比例)。
- **适配**: `object-cover`,定位 `center center`
- **兜底**: 若无图片,使用品牌色或随机几何渐变背景。
@@ -36,16 +36,18 @@
- **通知策略**: 关注成功后默认开启“更新动态”站内信提醒。首次关注显示轻量 Popover 提示:"已为您开启更新提醒,[去设置] 可调整"。
- **分享入口**: 更多操作按钮 (`...`) 中包含 [分享主页],同时页面右下角常驻悬浮分享按钮 (FAB)。
- **分享模板**: "向你推荐一个优质创作者:[作者名] - [简介摘要] [URL]"。
- **统计数据交互**:
- **统计数据交互**:
- **关注/获赞**: 点击弹出对应列表浮窗 (Modal),支持快速浏览。
- **内容**: 点击自动切换至 "全部内容" Tab。
### 2.2 导航与筛选 (Sticky Navigation & Search)
...
### 2.3 内容展示逻辑 (Content Area)
#### B. "全部内容" Tab (All Posts)
- **卡片内容类型标识 (Media Markers)**:
- **位置**: 封面图左下角。
- **视频**: `Play Icon` + `12:05` (时长)。
@@ -59,20 +61,25 @@
- **兜底**: 若价格数据异常缺失,默认显示为 "免费" 或 "价格待定"。
### 2.4 "关于" Tab & 更多操作
...
### 2.5 响应式与移动端适配 (lg 以下)
- **操作按钮布局**:
- 当屏幕宽度 `< 768px` 时,关注与私信按钮由水平排列改为 **通栏吸底 (Sticky Bottom)**,以保证单手操作便捷。
- 更多操作 (`...`) 变为右上角固定菜单。
- **统计数据**: 缩放字号并横向等分排列。
#### D. "关于" Tab (About)
- **内容层级**:
- **排版**: 使用 `prose` 规范,阅读行宽限制在 **800px** 以内。
- **图片**: 图片说明文字居中 `text-sm text-slate-400 mt-2`
- **联系方式**: 脱敏展示,点击 [查看] 需验证。
### 2.5 侧边引导与辅助
- **分享入口**: 右下角悬浮分享按钮 (FAB)。
- **推荐区块**: 若采用 9:3 布局,右侧展示 "推荐关注"。
- **订阅提醒**: 未关注用户滚到底部时弹出轻量订阅 Toast。
- **订阅提醒**: 未关注用户滚到底部时弹出轻量订阅 Toast。

View File

@@ -1,7 +1,6 @@
# 页面设计: 租户管理/创作者中心 (Tenant Management)
> **路由**: `/me/management/*`
> **布局**: `LayoutMain` (复用门户布局,采用侧边栏分栏结构)
> **路由**: `/creator/*` > **布局**: `LayoutMain` (复用门户布局,采用侧边栏分栏结构)
> **结构**: 与个人中心一致的左侧导航布局,保持视觉与操作连续性
## 1. 布局结构 (Structure)
@@ -9,7 +8,7 @@
管理模块直接集成在 C 端门户中,作为“创作者中心”存在。
- **导航栏**: 复用全局 `TopNavbar`,用户可通过头像下拉菜单的“管理入口”进入。
- **页面框架**:
- **页面框架**:
- **左侧 (Sidebar)**: 独立于个人中心的管理菜单(内容管理、订单管理、租户设置)。
- **右侧 (Main)**: 管理列表或表单容器,背景为 `bg-white` 圆角卡片。
@@ -17,7 +16,8 @@
## 2. 核心功能模块
### 2.1 管理概览 (Dashboard) `/me/management`
### 2.1 管理概览 (Dashboard) `/creator`
- **核心指标卡片 (Key Metrics)**:
- **总关注用户**: 显示累计粉丝数及较前一日的增长变化 (+/-)。
- **订单收益**:
@@ -28,12 +28,13 @@
- [待处理订单]: 待发货或退款申请中的实时数量提示。
- **内容表现**: 累计阅读量 (PV) 与 获赞总数。
- **趋势分析图表 (Charts)**:
- **收益/订单趋势**: 支持切换 [近7天] / [近30天] 的柱状图,直观展示收入波动。
- **收益/订单趋势**: 支持切换 [近 7 天] / [近 30 天] 的柱状图,直观展示收入波动。
- **粉丝增长曲线**: 折线图展示每日新增粉丝趋势。
- **快捷入口**: [发布新内容] [处理退款申请]。
- **公告通知**: 创作者专属公告(如:平台分成比例调整通知)。
### 2.2 内容管理 (Content Management) `/me/management/contents`
### 2.2 内容管理 (Content Management) `/creator/contents`
- **列表展示**:
- **字段**: 封面 | **曲种/剧目** | 价格 | 状态 | 阅读/收益 | 操作。
- **状态说明 (Audit Status)**:
@@ -44,16 +45,16 @@
- **发布编辑器 (WYSIWYG Editor - 戏曲定制版)**:
- **头部元数据 (Metadata)**:
- **曲种**: (必选) 下拉选择后台配置的曲种 (如 京剧/越剧)。
- **标题组合**:
- [剧目名] (必填, 如《锁麟囊》)
- [选段名] (可选, 如"春秋亭")
- **标题组合**:
- [剧目名] (必填, 如《锁麟囊》)
- [选段名] (可选, 如"春秋亭")
- [附加信息] (可选, 如"程砚秋")
- *预览*: 自动拼接展示 `《锁麟囊》春秋亭 (程砚秋)`
- _预览_: 自动拼接展示 `《锁麟囊》春秋亭 (程砚秋)`
- **工具栏功能**:
- **基础排版**: H1/H2/H3, 加粗, 下划线, 列表, 居中/对齐。
- **戏曲专用**:
- **剧本模式**: 专门的 [角色: 台词] 格式块,自动悬挂缩进。
- **唱段属性**: 插入音频时可选填 **定调 (Key)** (如 C大调/D大调) 和 **板式** (如 原板/流水)。
- **唱段属性**: 插入音频时可选填 **定调 (Key)** (如 C 大调/D 大调) 和 **板式** (如 原板/流水)。
- **注音工具**: 支持给生僻字添加拼音标注 (Ruby),解决尖团音等阅读障碍。
- **媒体 (仅支持上传)**: 图片, 视频 (支持断点续传), 音频, 附件。**不支持外链**。
- **操作**: 撤销, 重做, 清除格式, 插入分割线。
@@ -65,25 +66,26 @@
- **字段**:
- **封面图**: (必填) 点击上传,支持裁切预览。
- **摘要**: (可选) 自动提取前 50 字或手动输入。
- **戏曲属性**:
- **戏曲属性**:
- **主定调**: 下拉选择 (C/D/E/F/G/A/B 等)。
- **行当**: 生/旦/净/末/丑。
- **标签**: (可选) 搜索并多选,支持新增。
- **价格策略**:
- **价格策略**:
- Radio Group: [免费] | [付费] | [会员免费]。
- **付费设置**:
- **价格**: 输入金额 (¥)。
- **试看/试听**:
- **试看/试听**:
- Switch 开关 [开启试看]。
- 输入时长: 默认 `60` 秒 (支持自定义,针对戏曲长视频建议设为 3-5 分钟)。
- **操作**: [存草稿] [立即发布]。
### 2.3 订单管理 (Order Management) `/me/management/orders`
### 2.3 订单管理 (Order Management) `/creator/orders`
- **列表展示**:
- 记录所有购买该租户内容的订单。
- **字段**: 订单号 | 内容标题 | 买家昵称 | 实付金额 | 下单时间 | 状态。
- **管理干预 (Order Intervention)**:
- **状态流转**:
- **状态流转**:
- 正常: `已支付` -> `已完成`
- 退款: `退款申请中` (买家发起) -> `已退款` (同意) / `退款拒绝`
- **操作按钮**:
@@ -91,21 +93,24 @@
- [处理退款]: (仅针对退款申请) 弹出审核框,选择 [同意退款] 或 [拒绝]。
- [发送私信]: 快速联系买家。
### 2.4 租户资料设置 (Tenant Settings) `/me/management/settings`
### 2.4 租户资料设置 (Tenant Settings) `/creator/settings`
- **内容**:
- 修改租户主页的封面图 (Cover)、名称、简介。
- 资质材料上传(用于等级提升或认证)。
- **同步**: 此处修改后,租户公开主页 (`/tenants/:id`) 同步更新。
- **同步**: 此处修改后,租户公开主页 (`/t/:id`) 同步更新。
---
## 3. 交互细节与权限
### 3.1 权限拦截
- **访问控制**: 非租户管理员访问 `/me/management` 直接跳转至“租户申请”页或 403。
- **访问控制**: 非租户管理员访问 `/creator` 直接跳转至“租户申请”页或 403。
- **身份显示**: 在管理页面顶部显著位置显示“当前正在管理:[租户名称]”,防止多账号混淆。
### 3.2 适老化管理体验
- **大按钮**: 所有的“发布”、“编辑”按钮均采用 `h-12` 以上高度。
- **状态感知**: 每一个操作(如保存草稿)都有明确的 Toast 反馈。
- **自动保存**: 编辑器支持本地离线保存,防止误关闭导致内容丢失。

View File

@@ -1,798 +0,0 @@
# Sakai Vue - Admin Dashboard Template
Sakai Vue is a modern, production-ready admin dashboard template built with Vue 3, Vite, and PrimeVue. It provides a comprehensive set of UI components, pre-built layouts, and data services for rapidly developing enterprise-grade web applications. The template follows Vue 3 Composition API patterns with reactive state management and includes extensive component showcases demonstrating best practices.
The application features a modular architecture with five mock data services (Product, Customer, Country, Node, and Photo), a flexible theming system with three presets and 17 color schemes, and responsive layouts optimized for both desktop and mobile devices. It includes a complete CRUD implementation, dashboard with analytics widgets, 15+ UI component demo pages, authentication pages, and a public landing page, making it an ideal starting point for building admin panels, dashboards, and data-driven applications.
## Application Initialization
Vue 3 application setup with PrimeVue integration and global services
```javascript
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import Aura from '@primeuix/themes/aura';
import PrimeVue from 'primevue/config';
import ConfirmationService from 'primevue/confirmationservice';
import ToastService from 'primevue/toastservice';
import '@/assets/styles.scss';
const app = createApp(App);
app.use(router);
app.use(PrimeVue, {
theme: {
preset: Aura,
options: {
darkModeSelector: '.app-dark'
}
}
});
app.use(ToastService);
app.use(ConfirmationService);
app.mount('#app');
// Expected result: Application mounts to #app with PrimeVue Aura theme,
// Toast and Confirmation services available globally
```
## Product Service API
Mock data service providing product inventory with 30 sample products across four categories
```javascript
import { ProductService } from '@/service/ProductService';
// Fetch all products (30 items)
ProductService.getProducts()
.then(products => {
console.log(products);
// Returns array of products with structure:
// {
// id: '1000',
// code: 'f230fh0g3',
// name: 'Bamboo Watch',
// description: 'Product Description',
// image: 'bamboo-watch.jpg',
// price: 65,
// category: 'Accessories',
// quantity: 24,
// inventoryStatus: 'INSTOCK',
// rating: 5
// }
});
// Fetch products with order history
ProductService.getProductsWithOrders()
.then(products => {
products.forEach(product => {
console.log(`${product.name} has ${product.orders.length} orders`);
// Each order contains:
// { id, productCode, date, amount, quantity, customer, status }
});
});
// Fetch limited product sets
ProductService.getProductsMini() // First 5 products
.then(products => console.log(products.length)); // Output: 5
ProductService.getProductsSmall() // First 10 products
.then(products => console.log(products.length)); // Output: 10
// Available inventory statuses: 'INSTOCK', 'LOWSTOCK', 'OUTOFSTOCK'
// Available categories: 'Accessories', 'Fitness', 'Clothing', 'Electronics'
```
## Layout State Management Composable
Reactive layout configuration for theme switching, menu management, and dark mode
```javascript
import { useLayout } from '@/layout/composables/layout';
const {
layoutConfig,
layoutState,
toggleMenu,
isSidebarActive,
isDarkTheme,
getPrimary,
getSurface,
setActiveMenuItem,
toggleDarkMode
} = useLayout();
// Access current configuration
console.log(layoutConfig.preset); // 'Aura'
console.log(layoutConfig.primary); // 'emerald'
console.log(layoutConfig.menuMode); // 'static' or 'overlay'
console.log(layoutConfig.darkTheme); // false
// Toggle dark mode with view transitions
toggleDarkMode();
// Result: Adds/removes 'app-dark' class from document element
// Uses View Transition API if supported for smooth animation
// Toggle menu (responsive behavior)
toggleMenu();
// Desktop (>991px): Toggles staticMenuDesktopInactive
// Mobile: Toggles staticMenuMobileActive
// Overlay mode: Toggles overlayMenuActive
// Check sidebar state
if (isSidebarActive.value) {
console.log('Sidebar is currently visible');
}
// Set active menu item
setActiveMenuItem({ value: 'dashboard' });
// Result: layoutState.activeMenuItem = 'dashboard'
// Access computed values
console.log(isDarkTheme.value); // true/false
console.log(getPrimary.value); // 'emerald'
console.log(getSurface.value); // null or surface palette name
```
## Router Configuration
Vue Router setup with lazy-loaded components and nested routes
```javascript
import { createRouter, createWebHistory } from 'vue-router';
import AppLayout from '@/layout/AppLayout.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: AppLayout,
children: [
{
path: '/',
name: 'dashboard',
component: () => import('@/views/Dashboard.vue')
},
{
path: '/pages/crud',
name: 'crud',
component: () => import('@/views/pages/Crud.vue')
},
{
path: '/uikit/table',
name: 'table',
component: () => import('@/views/uikit/TableDoc.vue')
}
// ... 15+ UI kit routes
]
},
{
path: '/landing',
name: 'landing',
component: () => import('@/views/pages/Landing.vue')
},
{
path: '/auth/login',
name: 'login',
component: () => import('@/views/pages/auth/Login.vue')
}
]
});
// Navigate programmatically
import { useRouter } from 'vue-router';
const router = useRouter();
router.push({ name: 'dashboard' });
router.push('/pages/crud');
router.push({ name: 'table' });
// All routes use lazy loading for optimal code splitting
// Routes under AppLayout include sidebar and topbar navigation
```
## CRUD Operations Implementation
Complete product management with create, read, update, delete operations using PrimeVue DataTable
```javascript
<script setup>
import { ProductService } from '@/service/ProductService';
import { FilterMatchMode } from '@primevue/core/api';
import { useToast } from 'primevue/usetoast';
import { onMounted, ref } from 'vue';
const toast = useToast();
const products = ref([]);
const product = ref({});
const productDialog = ref(false);
const selectedProducts = ref();
const filters = ref({
global: { value: null, matchMode: FilterMatchMode.CONTAINS }
});
// Load products on component mount
onMounted(() => {
ProductService.getProducts().then((data) => {
products.value = data;
});
});
// Create new product
function openNew() {
product.value = {};
productDialog.value = true;
}
// Save product (create or update)
function saveProduct() {
if (product.value.name?.trim()) {
if (product.value.id) {
// Update existing product
const index = products.value.findIndex(p => p.id === product.value.id);
products.value[index] = product.value;
toast.add({
severity: 'success',
summary: 'Successful',
detail: 'Product Updated',
life: 3000
});
} else {
// Create new product
product.value.id = createId();
product.value.code = createId();
product.value.inventoryStatus = 'INSTOCK';
products.value.push(product.value);
toast.add({
severity: 'success',
summary: 'Successful',
detail: 'Product Created',
life: 3000
});
}
productDialog.value = false;
product.value = {};
}
}
// Update existing product
function editProduct(prod) {
product.value = { ...prod };
productDialog.value = true;
}
// Delete single product
function deleteProduct() {
products.value = products.value.filter((val) => val.id !== product.value.id);
toast.add({
severity: 'success',
summary: 'Successful',
detail: 'Product Deleted',
life: 3000
});
}
// Delete multiple products
function deleteSelectedProducts() {
products.value = products.value.filter((val) => !selectedProducts.value.includes(val));
selectedProducts.value = null;
toast.add({
severity: 'success',
summary: 'Successful',
detail: 'Products Deleted',
life: 3000
});
}
// Generate unique ID
function createId() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return Array.from({ length: 5 }, () =>
chars.charAt(Math.floor(Math.random() * chars.length))
).join('');
}
// Expected workflow:
// 1. Component loads products from service
// 2. User clicks "New" -> openNew() shows dialog
// 3. User fills form and saves -> saveProduct() creates product, shows toast
// 4. User clicks edit icon -> editProduct() pre-fills dialog with product data
// 5. User updates and saves -> saveProduct() updates product, shows toast
// 6. User clicks delete icon -> deleteProduct() removes product, shows toast
// 7. User selects multiple products and clicks delete -> deleteSelectedProducts()
</script>
```
## Dashboard Component Structure
Dashboard page composing five analytics widgets in responsive grid layout
```vue
<script setup>
import BestSellingWidget from '@/components/dashboard/BestSellingWidget.vue';
import NotificationsWidget from '@/components/dashboard/NotificationsWidget.vue';
import RecentSalesWidget from '@/components/dashboard/RecentSalesWidget.vue';
import RevenueStreamWidget from '@/components/dashboard/RevenueStreamWidget.vue';
import StatsWidget from '@/components/dashboard/StatsWidget.vue';
</script>
<template>
<div class="grid grid-cols-12 gap-8">
<!-- Stats cards spanning full width -->
<StatsWidget />
<!-- Two-column layout on extra large screens -->
<div class="col-span-12 xl:col-span-6">
<RecentSalesWidget />
<BestSellingWidget />
</div>
<div class="col-span-12 xl:col-span-6">
<RevenueStreamWidget />
<NotificationsWidget />
</div>
</div>
</template>
<!--
Responsive behavior:
- Mobile/Tablet: Single column, widgets stack vertically
- Desktop (xl): Two-column layout with widgets side by side
- StatsWidget shows 4 KPI cards: Orders, Revenue, Customers, Comments
- RecentSalesWidget shows table of recent sales transactions
- BestSellingWidget displays chart of best-selling products
- RevenueStreamWidget visualizes revenue breakdown
- NotificationsWidget lists recent system notifications
-->
```
## Customer Service API
Mock data service providing customer records with representative and activity data
```javascript
import { CustomerService } from '@/service/CustomerService';
CustomerService.getCustomers()
.then(customers => {
customers.forEach(customer => {
console.log({
id: customer.id,
name: customer.name,
country: customer.country, // { name: 'Algeria', code: 'dz' }
company: customer.company,
date: customer.date, // ISO date string
status: customer.status, // 'unqualified', 'negotiation', 'qualified', 'new'
verified: customer.verified, // boolean
activity: customer.activity, // 0-100
representative: {
name: customer.representative.name,
image: customer.representative.image
},
balance: customer.balance // number
});
});
});
// Usage in DataTable with filtering
<DataTable
:value="customers"
:filters="filters"
filterDisplay="row"
paginator
:rows="10"
>
<Column field="name" header="Name" sortable />
<Column field="country.name" header="Country" sortable />
<Column field="company" header="Company" sortable />
<Column field="status" header="Status">
<template #body="{ data }">
<Tag
:value="data.status"
:severity="getStatusSeverity(data.status)"
/>
</template>
</Column>
<Column field="verified" header="Verified" dataType="boolean" />
<Column field="balance" header="Balance" sortable>
<template #body="{ data }">
{{ formatCurrency(data.balance) }}
</template>
</Column>
</DataTable>
// Expected data structure: Array of customer objects with nested country
// and representative objects, suitable for DataTable display with badges
```
## Country Service API
Mock data service providing 249 world countries with ISO codes
```javascript
import { CountryService } from '@/service/CountryService';
CountryService.getCountries()
.then(countries => {
console.log(countries.length); // Output: 249
// Sample country structure
countries.forEach(country => {
console.log(country);
// { name: 'Afghanistan', code: 'af' }
// { name: 'United States', code: 'us' }
});
});
// Usage in Dropdown component
<script setup>
import { ref, onMounted } from 'vue';
import { CountryService } from '@/service/CountryService';
const countries = ref([]);
const selectedCountry = ref(null);
onMounted(() => {
CountryService.getCountries().then(data => {
countries.value = data;
});
});
</script>
<template>
<Dropdown
v-model="selectedCountry"
:options="countries"
optionLabel="name"
placeholder="Select a Country"
filter
class="w-full"
>
<template #value="slotProps">
<div v-if="slotProps.value">
{{ slotProps.value.name }}
</div>
<span v-else>
{{ slotProps.placeholder }}
</span>
</template>
<template #option="slotProps">
<div>{{ slotProps.option.name }}</div>
</template>
</Dropdown>
</template>
// Expected result: Dropdown with 249 countries, filterable search,
// returns selected country object with name and code properties
```
## Node Service API
Mock data service providing hierarchical tree structures for file browsers and navigation
```javascript
import { NodeService } from '@/service/NodeService';
// Tree structure for Tree component
NodeService.getTreeNodes()
.then(nodes => {
console.log(nodes);
// Returns 3 root nodes: Documents, Events, Movies
// Each with nested children representing folders/files
// Structure: { key, label, data, icon, children[] }
});
// Tree table structure with detailed data
NodeService.getTreeTableNodes()
.then(nodes => {
nodes.forEach(node => {
console.log({
key: node.key, // Unique identifier
data: {
name: node.data.name,
size: node.data.size,
type: node.data.type
},
children: node.children // Nested nodes
});
});
});
// Usage in TreeTable component
<script setup>
import { ref, onMounted } from 'vue';
import { NodeService } from '@/service/NodeService';
const nodes = ref([]);
onMounted(() => {
NodeService.getTreeTableNodes().then(data => {
nodes.value = data;
});
});
</script>
<template>
<TreeTable :value="nodes" :expandedKeys="expandedKeys">
<Column field="name" header="Name" expander />
<Column field="size" header="Size" />
<Column field="type" header="Type" />
</TreeTable>
</template>
// Expected result: 9 root nodes (Applications, Cloud, Desktop, Documents,
// Downloads, Main, Movies, Photos, Work) with nested children
// Perfect for file manager UIs and hierarchical data display
```
## Photo Service API
Mock data service providing gallery images hosted on CDN
```javascript
import { PhotoService } from '@/service/PhotoService';
PhotoService.getImages()
.then(images => {
console.log(images.length); // Output: 15
images.forEach(image => {
console.log({
itemImageSrc: image.itemImageSrc,
thumbnailImageSrc: image.thumbnailImageSrc,
alt: image.alt,
title: image.title
});
// Example output:
// {
// itemImageSrc: 'https://primefaces.org/cdn/primevue/images/galleria/galleria1.jpg',
// thumbnailImageSrc: 'https://primefaces.org/cdn/primevue/images/galleria/galleria1s.jpg',
// alt: 'Description for Image 1',
// title: 'Title 1'
// }
});
});
// Usage in Galleria component
<script setup>
import { ref, onMounted } from 'vue';
import { PhotoService } from '@/service/PhotoService';
const images = ref([]);
const activeIndex = ref(0);
onMounted(() => {
PhotoService.getImages().then(data => {
images.value = data;
});
});
</script>
<template>
<Galleria
:value="images"
v-model:activeIndex="activeIndex"
:numVisible="5"
containerStyle="max-width: 640px"
:showThumbnails="true"
:showItemNavigators="true"
>
<template #item="slotProps">
<img
:src="slotProps.item.itemImageSrc"
:alt="slotProps.item.alt"
style="width: 100%; display: block"
/>
</template>
<template #thumbnail="slotProps">
<img
:src="slotProps.item.thumbnailImageSrc"
:alt="slotProps.item.alt"
style="display: block"
/>
</template>
</Galleria>
</template>
// Expected result: Image gallery with 15 photos, thumbnail navigation,
// full-size image display, navigation arrows, responsive layout
```
## Theme Configuration and Dynamic Switching
Real-time theme customization with preset, color, and surface palette options
```javascript
import { useLayout } from '@/layout/composables/layout';
const { layoutConfig } = useLayout();
// Available theme presets
const presets = ['Aura', 'Lara', 'Nora'];
// Available primary colors
const primaryColors = [
'noir', 'emerald', 'green', 'lime', 'orange', 'amber',
'yellow', 'teal', 'cyan', 'sky', 'blue', 'indigo',
'violet', 'purple', 'fuchsia', 'pink', 'rose'
];
// Available surface palettes
const surfacePalettes = [
'slate', 'gray', 'zinc', 'neutral',
'stone', 'soho', 'viva', 'ocean'
];
// Change theme preset
import { updatePreset } from '@primeuix/themes';
layoutConfig.preset = 'Lara';
updatePreset({
preset: 'Lara',
primary: layoutConfig.primary,
surface: layoutConfig.surface
});
// Change primary color
layoutConfig.primary = 'blue';
updatePreset({
preset: layoutConfig.preset,
primary: 'blue',
surface: layoutConfig.surface
});
// Change surface palette
import { updateSurfacePalette } from '@primeuix/themes';
layoutConfig.surface = 'slate';
updateSurfacePalette('slate');
// Toggle dark mode
import { toggleDarkMode } from '@/layout/composables/layout';
toggleDarkMode();
// Result: Adds/removes 'app-dark' class with smooth View Transition API animation
// Complete theme change example
function applyTheme(preset, primary, surface) {
layoutConfig.preset = preset;
layoutConfig.primary = primary;
layoutConfig.surface = surface;
updatePreset({ preset, primary, surface });
if (surface) {
updateSurfacePalette(surface);
}
}
applyTheme('Nora', 'purple', 'ocean');
// Result: Theme instantly updates across entire application
// All PrimeVue components reflect new colors and styling
```
## PrimeVue DataTable with Filtering and Pagination
Advanced data table with global search, column filtering, sorting, and pagination
```vue
<script setup>
import { ref, onMounted } from 'vue';
import { FilterMatchMode } from '@primevue/core/api';
import { ProductService } from '@/service/ProductService';
const products = ref([]);
const filters = ref({
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
name: { value: null, matchMode: FilterMatchMode.STARTS_WITH },
'category': { value: null, matchMode: FilterMatchMode.IN }
});
const categories = ref(['Accessories', 'Fitness', 'Clothing', 'Electronics']);
const selectedProducts = ref([]);
onMounted(() => {
ProductService.getProducts().then(data => {
products.value = data;
});
});
function formatCurrency(value) {
return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
}
function getStatusSeverity(status) {
switch (status) {
case 'INSTOCK': return 'success';
case 'LOWSTOCK': return 'warn';
case 'OUTOFSTOCK': return 'danger';
default: return null;
}
}
</script>
<template>
<DataTable
v-model:selection="selectedProducts"
v-model:filters="filters"
:value="products"
paginator
:rows="10"
:rowsPerPageOptions="[5, 10, 20, 50]"
filterDisplay="row"
:globalFilterFields="['name', 'category', 'description']"
sortMode="multiple"
>
<template #header>
<div class="flex justify-between">
<span class="text-xl">Products</span>
<InputText
v-model="filters.global.value"
placeholder="Global Search"
/>
</div>
</template>
<Column selectionMode="multiple" headerStyle="width: 3rem" />
<Column field="name" header="Name" sortable>
<template #filter="{ filterModel, filterCallback }">
<InputText
v-model="filterModel.value"
@input="filterCallback()"
placeholder="Search by name"
/>
</template>
</Column>
<Column field="category" header="Category" sortable>
<template #filter="{ filterModel, filterCallback }">
<MultiSelect
v-model="filterModel.value"
@change="filterCallback()"
:options="categories"
placeholder="Any"
/>
</template>
</Column>
<Column field="price" header="Price" sortable>
<template #body="{ data }">
{{ formatCurrency(data.price) }}
</template>
</Column>
<Column field="inventoryStatus" header="Status" sortable>
<template #body="{ data }">
<Tag
:value="data.inventoryStatus"
:severity="getStatusSeverity(data.inventoryStatus)"
/>
</template>
</Column>
<Column header="Actions">
<template #body="{ data }">
<Button icon="pi pi-pencil" class="mr-2" @click="editProduct(data)" />
<Button icon="pi pi-trash" severity="danger" @click="deleteProduct(data)" />
</template>
</Column>
</DataTable>
</template>
<!--
Features demonstrated:
- Global search across multiple fields
- Column-specific filters (text input, multiselect)
- Multi-column sorting
- Row selection with checkboxes
- Pagination with configurable rows per page
- Custom cell rendering (currency, badges, action buttons)
- Responsive layout
-->
```
## Summary
Sakai Vue serves as a comprehensive foundation for building modern admin dashboards and data-driven web applications with Vue 3. The template demonstrates enterprise-grade patterns including service-based architecture for data management, reactive state with composables, and extensive PrimeVue component integration. The five mock data services (Product, Customer, Country, Node, Photo) provide realistic sample data with proper structure for immediate prototyping, while the complete CRUD implementation showcases best practices for data manipulation with user feedback through toasts and confirmations.
The template's standout features include its flexible theming system supporting three presets (Aura, Lara, Nora) with 17 primary colors and 8 surface palettes, all switchable in real-time without page reloads. The responsive layout system adapts seamlessly from mobile to desktop with static and overlay menu modes, dark mode support with smooth view transitions, and a component library of 15+ UI showcase pages demonstrating every major PrimeVue component. Whether building internal tools, customer portals, or SaaS applications, Sakai Vue provides production-ready code, professional design, and extensive examples that accelerate development from concept to deployment.

24
frontend/portal/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>portal</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

2580
frontend/portal/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
{
"name": "portal",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@primevue/themes": "^4.5.4",
"@tailwindcss/vite": "^4.1.18",
"pinia": "^3.0.4",
"primeicons": "^7.0.0",
"primevue": "^4.5.4",
"sass": "^1.97.1",
"tailwindcss": "^4.1.18",
"tailwindcss-animate": "^1.0.7",
"vue": "^3.5.24",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.2.4"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
<template>
<router-view />
</template>

View File

@@ -0,0 +1,30 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
:root {
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-200: #bfdbfe;
--color-primary-300: #93c5fd;
--color-primary-400: #60a5fa;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-primary-800: #1e40af;
--color-primary-900: #1e3a8a;
--color-primary-950: #172554;
}
@theme {
--color-primary-50: var(--color-primary-50);
--color-primary-100: var(--color-primary-100);
--color-primary-200: var(--color-primary-200);
--color-primary-300: var(--color-primary-300);
--color-primary-400: var(--color-primary-400);
--color-primary-500: var(--color-primary-500);
--color-primary-600: var(--color-primary-600);
--color-primary-700: var(--color-primary-700);
--color-primary-800: var(--color-primary-800);
--color-primary-900: var(--color-primary-900);
--color-primary-950: var(--color-primary-950);
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,57 @@
<template>
<footer class="bg-slate-900 text-slate-400 mt-auto">
<div class="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Brand -->
<div class="col-span-1">
<div class="flex items-center gap-2 mb-4">
<div class="w-8 h-8 bg-white/10 rounded-lg flex items-center justify-center text-white font-bold text-xl">Q</div>
<span class="text-xl font-bold text-white">Quyun</span>
</div>
<p class="text-sm leading-relaxed mb-6">专业的租户管理与内容交付平台连接创作者与用户探索内容的无限可能</p>
<div class="flex gap-4">
<a href="#" class="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center hover:bg-white/20 hover:text-white transition-all"><i class="pi pi-twitter"></i></a>
<a href="#" class="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center hover:bg-white/20 hover:text-white transition-all"><i class="pi pi-github"></i></a>
<a href="#" class="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center hover:bg-white/20 hover:text-white transition-all"><i class="pi pi-discord"></i></a>
</div>
</div>
<!-- Links -->
<div>
<h3 class="text-white font-bold mb-4">关于我们</h3>
<ul class="space-y-3 text-sm">
<li><a href="#" class="hover:text-white transition-colors">平台介绍</a></li>
<li><a href="#" class="hover:text-white transition-colors">加入我们</a></li>
<li><a href="#" class="hover:text-white transition-colors">联系方式</a></li>
<li><a href="#" class="hover:text-white transition-colors">合作伙伴</a></li>
</ul>
</div>
<div>
<h3 class="text-white font-bold mb-4">帮助中心</h3>
<ul class="space-y-3 text-sm">
<li><a href="#" class="hover:text-white transition-colors">用户指南</a></li>
<li><a href="#" class="hover:text-white transition-colors">创作者手册</a></li>
<li><a href="#" class="hover:text-white transition-colors">常见问题</a></li>
<li><a href="#" class="hover:text-white transition-colors">反馈建议</a></li>
</ul>
</div>
<div>
<h3 class="text-white font-bold mb-4">法律条款</h3>
<ul class="space-y-3 text-sm">
<li><a href="#" class="hover:text-white transition-colors">用户协议</a></li>
<li><a href="#" class="hover:text-white transition-colors">隐私政策</a></li>
<li><a href="#" class="hover:text-white transition-colors">知识产权</a></li>
<li><a href="#" class="hover:text-white transition-colors">社区规范</a></li>
</ul>
</div>
</div>
<div class="border-t border-slate-800 mt-12 pt-8 flex flex-col md:flex-row justify-between items-center text-xs">
<p>© 2025 Quyun Platform. All rights reserved.</p>
<p class="mt-2 md:mt-0">ICP 88888888 -1 | 公安网备 11010102000000 </p>
</div>
</div>
</footer>
</template>

View File

@@ -0,0 +1,43 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<nav class="fixed top-0 w-full z-50 bg-white border-b border-slate-200 h-16">
<div class="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8 h-full flex items-center justify-between">
<!-- Left: Logo -->
<router-link to="/" class="flex items-center gap-2">
<div class="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center text-white font-bold text-xl">Q</div>
<span class="text-xl font-bold text-slate-900 hidden sm:block">Quyun</span>
</router-link>
<!-- Center-Left: Nav Links (Desktop) -->
<div class="hidden md:flex items-center space-x-8">
<router-link to="/" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">首页</router-link>
<router-link to="/explore" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">发现</router-link>
<router-link to="/topics" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">专题</router-link>
</div>
<!-- Center-Right: Global Search -->
<div class="hidden sm:flex flex-1 max-w-md mx-8">
<div class="relative w-full">
<i class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
<input
type="text"
placeholder="搜索感兴趣的内容..."
class="w-full h-10 pl-10 pr-4 rounded-full bg-slate-100 border-none focus:bg-white focus:ring-2 focus:ring-primary-100 text-sm transition-all"
>
</div>
</div>
<!-- Right: User Actions -->
<div class="flex items-center gap-4">
<template v-if="isLoggedIn">
<!-- Notification -->
<router-link to="/me/notifications" class="relative w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-50 text-slate-600">
<i class="pi pi-bell text-xl"></i>
<span class="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border border-white"></span>
</router-link>
<!-- Creator Entry -->
<router-link to="/creator/apply" class="hidden sm:flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-50 rounded-lg border border-slate-200">
<i class="pi pi-pencil"></i>
<span>创作</span>
</router-link>
<!-- Avatar Dropdown -->
<div class="relative group h-full flex items-center">
<button class="w-9 h-9 rounded-full overflow-hidden border border-slate-200 focus:ring-2 ring-primary-100">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Felix" alt="User" class="w-full h-full object-cover">
</button>
<!-- Dropdown Menu (Mock) -->
<!-- Added pt-2 to create a safe hover zone bridge -->
<div class="absolute right-0 top-full pt-2 w-48 hidden group-hover:block">
<div class="bg-white rounded-xl shadow-lg border border-slate-100 py-1">
<div class="px-4 py-3 border-b border-slate-50">
<p class="text-sm font-bold text-slate-900">Felix Demo</p>
<p class="text-xs text-slate-500 truncate">felix@example.com</p>
</div>
<router-link to="/me" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">个人中心</router-link>
<router-link to="/creator" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">创作者中心</router-link>
<div class="border-t border-slate-50 mt-1"></div>
<button @click="logout" class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50">退出登录</button>
</div>
</div>
</div>
</template>
<template v-else>
<router-link to="/auth/login" class="text-slate-600 font-medium hover:text-primary-600 px-3 py-2">登录</router-link>
<router-link to="/auth/login" class="bg-primary-600 text-white px-5 py-2 rounded-full font-medium hover:bg-primary-700 transition-colors">注册</router-link>
</template>
<!-- Mobile Menu Button -->
<button class="md:hidden w-10 h-10 flex items-center justify-center text-slate-600">
<i class="pi pi-bars text-xl"></i>
</button>
</div>
</div>
</nav>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const isLoggedIn = ref(true); // Mock login state
const router = useRouter();
const logout = () => {
isLoggedIn.value = false;
router.push('/');
};
</script>

View File

@@ -0,0 +1,5 @@
<template>
<div class="min-h-screen bg-slate-50 flex items-center justify-center p-4">
<router-view />
</div>
</template>

View File

@@ -0,0 +1,14 @@
<template>
<div class="min-h-screen flex flex-col bg-slate-50">
<TopNavbar />
<main class="flex-grow pt-16">
<router-view />
</main>
<AppFooter />
</div>
</template>
<script setup>
import TopNavbar from '../components/TopNavbar.vue';
import AppFooter from '../components/AppFooter.vue';
</script>

View File

@@ -0,0 +1,68 @@
<template>
<div class="min-h-screen flex flex-col bg-slate-50">
<TopNavbar />
<main class="flex-grow pt-16">
<div class="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8 py-8 flex gap-8">
<!-- Sidebar -->
<aside class="w-[280px] flex-shrink-0 hidden lg:block">
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden sticky top-24">
<!-- User Brief -->
<div class="p-6 border-b border-slate-100 bg-slate-50/50">
<div class="flex items-center gap-4">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Felix" class="w-12 h-12 rounded-full border-2 border-white shadow-sm" />
<div class="overflow-hidden">
<div class="font-bold text-slate-900 truncate">Felix Demo</div>
<div class="text-xs text-slate-500">ID: 9527330</div>
</div>
</div>
</div>
<!-- Menus -->
<nav class="p-4 space-y-1">
<router-link to="/me" exact-active-class="bg-primary-50 text-primary-600 font-semibold" class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
<i class="pi pi-home text-lg"></i>
<span>概览</span>
</router-link>
<router-link to="/me/orders" active-class="bg-primary-50 text-primary-600 font-semibold" class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
<i class="pi pi-shopping-bag text-lg"></i>
<span>我的订单</span>
</router-link>
<router-link to="/me/wallet" active-class="bg-primary-50 text-primary-600 font-semibold" class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
<i class="pi pi-wallet text-lg"></i>
<span>我的钱包</span>
</router-link>
<router-link to="/me/library" active-class="bg-primary-50 text-primary-600 font-semibold" class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
<i class="pi pi-book text-lg"></i>
<span>已购内容</span>
</router-link>
<router-link to="/me/notifications" active-class="bg-primary-50 text-primary-600 font-semibold" class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
<i class="pi pi-bell text-lg"></i>
<span>消息中心</span>
</router-link>
<div class="my-2 border-t border-slate-100"></div>
<router-link to="/me/profile" active-class="bg-primary-50 text-primary-600 font-semibold" class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
<i class="pi pi-user text-lg"></i>
<span>个人资料</span>
</router-link>
<router-link to="/me/security" active-class="bg-primary-50 text-primary-600 font-semibold" class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
<i class="pi pi-shield text-lg"></i>
<span>账号安全</span>
</router-link>
</nav>
</div>
</aside>
<!-- Main Content -->
<div class="flex-grow min-w-0">
<router-view />
</div>
</div>
</main>
<AppFooter />
</div>
</template>
<script setup>
import TopNavbar from '../components/TopNavbar.vue';
import AppFooter from '../components/AppFooter.vue';
</script>

View File

@@ -0,0 +1,26 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import PrimeVue from 'primevue/config';
import Aura from '@primevue/themes/aura';
import App from './App.vue';
import router from './router';
import './assets/main.css';
import 'primeicons/primeicons.css';
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.use(PrimeVue, {
theme: {
preset: Aura,
options: {
prefix: 'p',
darkModeSelector: '.my-app-dark',
cssLayer: false
}
}
});
app.mount('#app');

View File

@@ -0,0 +1,166 @@
import { createRouter, createWebHistory } from 'vue-router'
import LayoutMain from '../layout/LayoutMain.vue'
import LayoutAuth from '../layout/LayoutAuth.vue'
import LayoutUser from '../layout/LayoutUser.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: LayoutMain,
children: [
{
path: '',
name: 'home',
component: () => import('../views/HomeView.vue')
},
{
path: 'contents/:id',
name: 'content-detail',
component: () => import('../views/content/DetailView.vue')
},
{
path: 't/:id',
name: 'tenant-home',
component: () => import('../views/tenant/HomeView.vue')
},
{
path: 'explore',
name: 'explore',
component: () => import('../views/ExploreView.vue') // Placeholder
},
{
path: 'topics',
name: 'topics',
component: () => import('../views/TopicsView.vue') // Placeholder
},
{
path: 'creator/apply',
name: 'creator-apply',
component: () => import('../views/creator/ApplyView.vue')
}
]
},
{
path: '/auth',
component: LayoutAuth,
children: [
{
path: 'login',
name: 'login',
component: () => import('../views/auth/LoginView.vue')
}
]
},
{
path: '/me',
component: LayoutUser,
children: [
{
path: '',
name: 'user-dashboard',
component: () => import('../views/user/DashboardView.vue')
},
{
path: 'orders',
name: 'user-orders',
component: () => import('../views/user/OrdersView.vue')
},
{
path: 'orders/:id',
name: 'user-order-detail',
component: () => import('../views/order/DetailView.vue')
},
{
path: 'wallet',
name: 'user-wallet',
component: () => import('../views/user/WalletView.vue') // Placeholder
},
{
path: 'library',
name: 'user-library',
component: () => import('../views/user/LibraryView.vue') // Placeholder
},
{
path: 'notifications',
name: 'user-notifications',
component: () => import('../views/user/NotificationsView.vue') // Placeholder
},
{
path: 'profile',
name: 'user-profile',
component: () => import('../views/user/ProfileView.vue') // Placeholder
},
{
path: 'security',
name: 'user-security',
component: () => import('../views/user/SecurityView.vue') // Placeholder
}
]
},
{
path: '/creator',
component: LayoutUser, // Initially use LayoutUser, later maybe specialized LayoutCreator
children: [
{
path: '',
name: 'creator-dashboard',
component: () => import('../views/creator/DashboardView.vue')
},
{
path: 'contents',
name: 'creator-contents',
component: () => import('../views/creator/ContentsView.vue')
},
{
path: 'orders',
name: 'creator-orders',
component: () => import('../views/creator/OrdersView.vue')
},
{
path: 'settings',
name: 'creator-settings',
component: () => import('../views/creator/SettingsView.vue')
}
]
},
{
path: '/checkout',
component: LayoutMain, // Or a simplified checkout layout
children: [
{
path: '',
name: 'checkout',
component: () => import('../views/order/CheckoutView.vue')
}
]
},
{
path: '/payment/:id',
component: LayoutMain,
children: [
{
path: '',
name: 'payment',
component: () => import('../views/order/PaymentView.vue')
}
]
},
// Fallback
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('../views/misc/NotFoundView.vue')
}
],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
export default router

View File

@@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -0,0 +1,6 @@
<template>
<div class="mx-auto max-w-screen-xl my-8 p-8 bg-white rounded-xl shadow-sm">
<h1 class="text-2xl font-bold mb-4">Explore</h1>
<p class="text-slate-400">(Explore content)</p>
</div>
</template>

View File

@@ -0,0 +1,224 @@
<template>
<div class="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8 py-8">
<!-- Hero Banner -->
<div class="relative w-full h-[400px] rounded-2xl overflow-hidden bg-slate-900 mb-8 group">
<!-- Mock Carousel Image -->
<img src="https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80" class="w-full h-full object-cover opacity-80 transition-transform duration-700 group-hover:scale-105" alt="Banner">
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent"></div>
<div class="absolute bottom-0 left-0 p-10 max-w-2xl text-white">
<div class="inline-block px-3 py-1 bg-red-600 text-white text-xs font-bold rounded mb-3">置顶推荐</div>
<h2 class="text-4xl font-bold mb-4 leading-tight">京剧霸王别姬全本实录程派艺术的巅峰演绎</h2>
<p class="text-lg text-slate-200 line-clamp-2">梅兰芳大师经典之作高清修复版独家上线感受国粹魅力重温梨园风华</p>
</div>
<!-- Arrows (Always visible as per spec) -->
<button class="absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/30 hover:bg-black/50 text-white rounded-full flex items-center justify-center backdrop-blur-sm transition-all"><i class="pi pi-chevron-left text-xl"></i></button>
<button class="absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/30 hover:bg-black/50 text-white rounded-full flex items-center justify-center backdrop-blur-sm transition-all"><i class="pi pi-chevron-right text-xl"></i></button>
<!-- Indicators -->
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
<span class="w-2 h-2 rounded-full bg-white"></span>
<span class="w-2 h-2 rounded-full bg-white/50"></span>
<span class="w-2 h-2 rounded-full bg-white/50"></span>
</div>
</div>
<!-- Filter Bar -->
<div class="mb-8">
<div class="flex items-center gap-8 border-b border-slate-200 pb-4 mb-4">
<button class="text-lg font-bold text-primary-600 border-b-2 border-primary-600 -mb-4.5 pb-4 px-2">推荐</button>
<button class="text-lg font-medium text-slate-500 hover:text-slate-800 -mb-4.5 pb-4 px-2 transition-colors">最新</button>
<button class="text-lg font-medium text-slate-500 hover:text-slate-800 -mb-4.5 pb-4 px-2 transition-colors">热门</button>
</div>
<!-- Tags -->
<div class="flex flex-wrap gap-3">
<button class="px-4 py-1.5 rounded-full bg-slate-900 text-white text-sm font-medium">全部</button>
<button class="px-4 py-1.5 rounded-full bg-slate-100 text-slate-600 hover:bg-slate-200 text-sm font-medium transition-colors">京剧</button>
<button class="px-4 py-1.5 rounded-full bg-slate-100 text-slate-600 hover:bg-slate-200 text-sm font-medium transition-colors">昆曲</button>
<button class="px-4 py-1.5 rounded-full bg-slate-100 text-slate-600 hover:bg-slate-200 text-sm font-medium transition-colors">越剧</button>
<button class="px-4 py-1.5 rounded-full bg-slate-100 text-slate-600 hover:bg-slate-200 text-sm font-medium transition-colors">名家名段</button>
<button class="px-4 py-1.5 rounded-full bg-slate-100 text-slate-600 hover:bg-slate-200 text-sm font-medium transition-colors">戏曲教学</button>
</div>
</div>
<!-- Main Layout: Grid 9:3 -->
<div class="grid grid-cols-12 gap-8">
<!-- Main Feed (Left 9) -->
<div class="col-span-12 lg:col-span-8 xl:col-span-9 space-y-6">
<!-- Card Variant 1: Single Image (Right) -->
<router-link to="/contents/1" class="block bg-white rounded-xl shadow-sm border border-slate-100 p-5 hover:shadow-md transition-shadow group">
<div class="flex gap-6">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-2">
<span class="px-1.5 py-0.5 rounded text-xs font-medium bg-red-50 text-red-600 border border-red-100">置顶</span>
<span class="text-xs text-slate-500 border border-slate-200 px-1 rounded">[京剧]</span>
</div>
<h3 class="text-lg font-bold text-slate-900 mb-2 leading-snug group-hover:text-primary-600 transition-colors">锁麟囊选段春秋亭外风雨暴 (张火丁亲授版)</h3>
<p class="text-base text-slate-500 line-clamp-2 mb-4 leading-relaxed">张火丁教授亲自讲解程派发音技巧深度剖析锁麟囊中春秋亭一折的唱腔设计与情感表达包含完整示范与逐句拆解</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3 text-sm text-slate-500">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Zhang" class="w-6 h-6 rounded-full">
<span>张火丁工作室</span>
<span class="w-1 h-1 bg-slate-300 rounded-full"></span>
<span>行当青衣</span>
<span class="w-1 h-1 bg-slate-300 rounded-full"></span>
<span>2小时前</span>
</div>
<div class="flex items-center gap-4">
<span class="text-sm text-slate-400"><i class="pi pi-eye mr-1"></i> 1.2</span>
<span class="text-lg font-bold text-red-600">¥ 9.90</span>
</div>
</div>
</div>
<div class="w-[240px] h-[135px] flex-shrink-0 rounded-lg overflow-hidden relative bg-slate-100 hidden sm:block">
<img src="https://images.unsplash.com/photo-1576014131795-d44019d02374?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black/20 flex items-center justify-center">
<i class="pi pi-play-circle text-4xl text-white opacity-80"></i>
</div>
<span class="absolute bottom-2 right-2 px-1.5 py-0.5 bg-black/60 text-white text-xs rounded">15:30</span>
</div>
</div>
</router-link>
<!-- Card Variant 2: No Image (Text Only) -->
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-5 hover:shadow-md transition-shadow group">
<div class="flex items-center gap-2 mb-2">
<span class="px-1.5 py-0.5 rounded text-xs font-medium bg-green-50 text-green-600 border border-green-100">限免</span>
<span class="text-xs text-slate-500 border border-slate-200 px-1 rounded">[昆曲]</span>
</div>
<h3 class="text-lg font-bold text-slate-900 mb-3 group-hover:text-primary-600 transition-colors">浅谈昆曲牡丹亭中的水磨腔艺术特点</h3>
<p class="text-base text-slate-500 line-clamp-3 mb-4 leading-relaxed">昆曲之所以被称为百戏之祖其细腻婉转的水磨腔功不可没本文将从发音吐字行腔三个维度带您领略昆曲的声韵之美对于初学者来说掌握字头字腹字尾的处理是关键...</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3 text-sm text-slate-500">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Li" class="w-6 h-6 rounded-full">
<span>梨园小生</span>
<span class="w-1 h-1 bg-slate-300 rounded-full"></span>
<span>昨天</span>
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-slate-400 line-through">¥ 5.00</span>
<span class="text-sm font-bold text-green-600 border border-green-200 px-2 py-0.5 rounded">限时免费</span>
</div>
</div>
</div>
<!-- Card Variant 3: 3 Images -->
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-5 hover:shadow-md transition-shadow group">
<h3 class="text-lg font-bold text-slate-900 mb-3 group-hover:text-primary-600 transition-colors">[图集] 2024 新年京剧晚会后台探班名角云集</h3>
<div class="grid grid-cols-3 gap-2 mb-4">
<div class="aspect-[4/3] rounded-lg overflow-hidden bg-slate-100">
<img src="https://images.unsplash.com/photo-1469571486292-0ba58a3f068b?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60" class="w-full h-full object-cover hover:scale-105 transition-transform duration-500">
</div>
<div class="aspect-[4/3] rounded-lg overflow-hidden bg-slate-100">
<img src="https://images.unsplash.com/photo-1533174072545-e8d4aa97edf9?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60" class="w-full h-full object-cover hover:scale-105 transition-transform duration-500">
</div>
<div class="aspect-[4/3] rounded-lg overflow-hidden bg-slate-100 relative">
<img src="https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60" class="w-full h-full object-cover hover:scale-105 transition-transform duration-500">
<div class="absolute bottom-2 right-2 px-1.5 py-0.5 bg-black/60 text-white text-xs rounded">+9</div>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3 text-sm text-slate-500">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Photo" class="w-6 h-6 rounded-full">
<span>戏曲摄影师老王</span>
<span class="w-1 h-1 bg-slate-300 rounded-full"></span>
<span>3天前</span>
</div>
<span class="text-sm text-slate-400"><i class="pi pi-eye mr-1"></i> 8.5k</span>
</div>
</div>
<!-- Load More -->
<div class="pt-4 text-center">
<button class="px-8 py-3 bg-white border border-slate-200 rounded-full text-slate-600 hover:bg-slate-50 hover:text-primary-600 font-medium transition-all shadow-sm">
点击加载更多内容
</button>
</div>
</div>
<!-- Sidebar (Right 3) -->
<div class="hidden lg:block lg:col-span-4 xl:col-span-3 space-y-6">
<!-- Announcement -->
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<h3 class="font-bold text-slate-900 mb-4 flex items-center gap-2">
<i class="pi pi-megaphone text-orange-500"></i> 公告
</h3>
<ul class="space-y-3 text-sm text-slate-600">
<li class="line-clamp-1 hover:text-primary-600 cursor-pointer"> 关于调整创作者收益结算周期的通知</li>
<li class="line-clamp-1 hover:text-primary-600 cursor-pointer"> 国粹传承戏曲短视频大赛开启</li>
<li class="line-clamp-1 hover:text-primary-600 cursor-pointer"> 平台系统维护升级公告 (12.30)</li>
</ul>
</div>
<!-- Recommended Tenants -->
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<h3 class="font-bold text-slate-900 mb-4">推荐名家</h3>
<div class="space-y-4">
<div class="flex items-center gap-3">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Master1" class="w-10 h-10 rounded-full">
<div class="flex-1 min-w-0">
<div class="font-bold text-slate-900 text-sm truncate">梅派传人小林</div>
<div class="text-xs text-slate-500 truncate">粉丝 12.5</div>
</div>
<button class="px-3 py-1 bg-primary-50 text-primary-600 text-xs font-bold rounded-full hover:bg-primary-100">关注</button>
</div>
<div class="flex items-center gap-3">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Master2" class="w-10 h-10 rounded-full">
<div class="flex-1 min-w-0">
<div class="font-bold text-slate-900 text-sm truncate">豫剧李大师</div>
<div class="text-xs text-slate-500 truncate">粉丝 8.9</div>
</div>
<button class="px-3 py-1 bg-primary-50 text-primary-600 text-xs font-bold rounded-full hover:bg-primary-100">关注</button>
</div>
<div class="flex items-center gap-3">
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=Master3" class="w-10 h-10 rounded-full">
<div class="flex-1 min-w-0">
<div class="font-bold text-slate-900 text-sm truncate">越剧小生阿强</div>
<div class="text-xs text-slate-500 truncate">粉丝 5.2</div>
</div>
<button class="px-3 py-1 bg-slate-100 text-slate-400 text-xs font-bold rounded-full">已关注</button>
</div>
</div>
</div>
<!-- Trending List -->
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-5">
<h3 class="font-bold text-slate-900 mb-4 flex items-center gap-2">
<i class="pi pi-chart-line text-red-500"></i> 本周热门
</h3>
<ul class="space-y-4">
<li class="flex gap-3 items-start">
<span class="text-red-500 font-bold italic text-lg w-4">1</span>
<div class="flex-1">
<h4 class="text-sm font-medium text-slate-800 line-clamp-2 hover:text-primary-600 cursor-pointer">智取威虎山选段今日痛饮庆功酒</h4>
<span class="text-xs text-slate-400 mt-1 block">15.2 阅读</span>
</div>
</li>
<li class="flex gap-3 items-start">
<span class="text-orange-500 font-bold italic text-lg w-4">2</span>
<div class="flex-1">
<h4 class="text-sm font-medium text-slate-800 line-clamp-2 hover:text-primary-600 cursor-pointer">深度解析京剧脸谱颜色的含义</h4>
<span class="text-xs text-slate-400 mt-1 block">9.8 阅读</span>
</div>
</li>
<li class="flex gap-3 items-start">
<span class="text-yellow-500 font-bold italic text-lg w-4">3</span>
<div class="flex-1">
<h4 class="text-sm font-medium text-slate-800 line-clamp-2 hover:text-primary-600 cursor-pointer">黄梅戏女驸马全场高清</h4>
<span class="text-xs text-slate-400 mt-1 block">7.5 阅读</span>
</div>
</li>
</ul>
</div>
<!-- Ad / Promo -->
<div class="rounded-xl overflow-hidden shadow-sm">
<img src="https://images.unsplash.com/photo-1557683316-973673baf926?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60" class="w-full h-40 object-cover">
<div class="bg-white p-3 flex justify-between items-center">
<span class="text-xs text-slate-400 border border-slate-200 px-1 rounded">广告</span>
<span class="text-sm font-medium text-slate-700">戏曲周边商城上线啦</span>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div class="mx-auto max-w-screen-xl my-8 p-8 bg-white rounded-xl shadow-sm">
<h1 class="text-2xl font-bold mb-4">Topics</h1>
<p class="text-slate-400">(Topic list)</p>
</div>
</template>

View File

@@ -0,0 +1,124 @@
<template>
<div class="bg-white rounded-2xl shadow-xl w-full max-w-4xl overflow-hidden flex min-h-[550px]">
<!-- Left Brand Area -->
<div class="hidden md:flex w-1/2 bg-slate-900 relative p-12 flex-col justify-between text-white">
<!-- Decor/Bg could be here -->
<div class="z-10">
<div class="flex items-center gap-2 mb-8">
<div class="w-8 h-8 bg-white/20 rounded flex items-center justify-center font-bold">Q</div>
<span class="text-xl font-bold">Quyun</span>
</div>
<h1 class="text-4xl font-bold leading-tight mb-4">探索戏曲的<br>无限可能</h1>
<p class="text-slate-400">专业的租户管理与内容交付平台连接创作者与用户</p>
</div>
<div class="text-xs text-slate-500 z-10">© 2025 Quyun. All rights reserved.</div>
</div>
<!-- Right Form Area -->
<div class="w-full md:w-1/2 p-8 sm:p-12 flex flex-col justify-center bg-white relative">
<div v-if="step === 1">
<h2 class="text-2xl font-bold text-slate-900 mb-2">欢迎回来</h2>
<p class="text-sm text-slate-500 mb-8">未注册的手机号验证后将自动创建账号</p>
<form @submit.prevent="getOTP">
<div class="mb-6">
<label class="block text-sm font-medium text-slate-700 mb-2">手机号码</label>
<div class="flex">
<span class="inline-flex items-center px-4 rounded-l-lg border border-r-0 border-slate-300 bg-slate-50 text-slate-500 text-sm">+86</span>
<input
v-model="phone"
type="tel"
class="flex-1 block w-full rounded-r-lg border-slate-300 focus:border-primary-500 focus:ring-primary-500 sm:text-lg py-3"
placeholder="请输入手机号"
required
>
</div>
</div>
<div class="flex items-start mb-6">
<div class="flex items-center h-5">
<input id="terms" v-model="agreed" type="checkbox" class="w-5 h-5 rounded border-slate-300 text-primary-600 focus:ring-primary-500">
</div>
<label for="terms" class="ml-3 text-sm text-slate-500">
我已阅读并同意 <a href="#" class="text-primary-600 hover:underline">用户协议</a> <a href="#" class="text-primary-600 hover:underline">隐私政策</a>
</label>
</div>
<button
type="submit"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-lg font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="!agreed || !phone"
>
获取验证码
</button>
</form>
<div class="mt-8">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-slate-200"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-slate-500">其他方式登录</span>
</div>
</div>
<div class="mt-6 flex justify-center gap-6">
<button class="w-10 h-10 rounded-full bg-slate-50 border border-slate-200 flex items-center justify-center hover:bg-slate-100 text-green-600"><i class="pi pi-wechat text-xl"></i></button>
<button class="w-10 h-10 rounded-full bg-slate-50 border border-slate-200 flex items-center justify-center hover:bg-slate-100 text-slate-800"><i class="pi pi-github text-xl"></i></button>
</div>
</div>
</div>
<div v-else-if="step === 2">
<button @click="step = 1" class="absolute top-8 left-8 text-slate-400 hover:text-slate-600"><i class="pi pi-arrow-left mr-1"></i> 返回</button>
<h2 class="text-2xl font-bold text-slate-900 mb-2">输入验证码</h2>
<p class="text-sm text-slate-500 mb-8">验证码已发送至 +86 {{ phone }}</p>
<div class="flex gap-3 mb-8 justify-center">
<input
v-for="i in 6" :key="i"
type="text"
maxlength="1"
class="w-12 h-14 text-center text-2xl font-bold border border-slate-300 rounded-lg focus:border-primary-500 focus:ring-2 focus:ring-primary-200"
>
</div>
<div class="text-center mb-8">
<button class="text-sm text-slate-500 hover:text-primary-600">59s 后重新获取</button>
</div>
<button
@click="login"
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-lg font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
登录 / 注册
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const step = ref(1);
const phone = ref('');
const agreed = ref(false);
const getOTP = () => {
if(!agreed.value) return;
// Simulate API call
setTimeout(() => {
step.value = 2;
}, 500);
};
const login = () => {
// Simulate Login
setTimeout(() => {
router.push('/');
}, 800);
};
</script>

View File

@@ -0,0 +1,10 @@
<template>
<div>
<!-- Placeholder for Content Detail View -->
<div class="bg-white p-8 rounded-xl shadow-sm border border-slate-100 mx-auto max-w-screen-xl my-8 text-center">
<h1 class="text-2xl font-bold mb-4">Content Detail Page</h1>
<p class="text-slate-500">ID: {{ $route.params.id }}</p>
<p class="text-slate-400 mt-2">(Implementation pending based on PAGE_CONTENT_DETAIL.md)</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<div class="mx-auto max-w-3xl my-12 bg-white rounded-xl shadow-sm border border-slate-100 p-8">
<h1 class="text-2xl font-bold mb-4 text-center">Creator Application</h1>
<p class="text-slate-500 text-center mb-8">Join us and start your creative journey.</p>
<p class="text-slate-400 text-center">(Implementation pending based on PAGE_TENANT_APPLY.md)</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">Creator Contents</h1>
<p class="text-slate-400">(List of contents)</p>
</div>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">Creator Dashboard</h1>
<p class="text-slate-500">Welcome to the Creator Center.</p>
<p class="text-slate-400 mt-2">(Implementation pending based on PAGE_TENANT_MANAGEMENT.md)</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">Creator Orders</h1>
<p class="text-slate-400">(List of orders)</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">Creator Settings</h1>
<p class="text-slate-400">(Tenant settings)</p>
</div>
</template>

View File

@@ -0,0 +1,16 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-slate-50">
<div class="text-center">
<div class="mb-6">
<!-- Use an image or illustration here -->
<i class="pi pi-compass text-6xl text-slate-300"></i>
</div>
<h1 class="text-4xl font-bold text-slate-900 mb-4">404</h1>
<p class="text-xl text-slate-600 mb-8">抱歉您访问的页面走丢了</p>
<div class="flex justify-center gap-4">
<router-link to="/" class="px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">返回首页</router-link>
<button @click="$router.back()" class="px-6 py-3 border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 transition-colors">返回上一页</button>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div class="mx-auto max-w-screen-xl my-8 p-8 bg-white rounded-xl shadow-sm">
<h1 class="text-2xl font-bold mb-4">Checkout</h1>
<p class="text-slate-400">(Checkout flow)</p>
</div>
</template>

View File

@@ -0,0 +1,173 @@
<template>
<div class="bg-white rounded-xl shadow-sm border border-slate-100 min-h-[600px] p-8">
<!-- Header -->
<div class="flex items-center gap-4 mb-8">
<button @click="$router.back()" class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 text-slate-500 transition-colors">
<i class="pi pi-arrow-left text-lg"></i>
</button>
<div>
<h1 class="text-2xl font-bold text-slate-900">订单详情</h1>
<p class="text-sm text-slate-500">订单号: {{ order.id }}</p>
</div>
<div class="ml-auto">
<span class="px-3 py-1 rounded-full text-sm font-bold bg-green-50 text-green-600 border border-green-100" v-if="order.status === 'completed'">交易成功</span>
<span class="px-3 py-1 rounded-full text-sm font-bold bg-orange-50 text-orange-600 border border-orange-100" v-else-if="order.status === 'unpaid'">待支付</span>
</div>
</div>
<!-- Status Progress (Fixed Alignment) -->
<div class="mb-10 p-8 bg-slate-50 rounded-xl border border-slate-100">
<div class="relative mx-12"> <!-- Added margin for text overflow safety -->
<!-- Background Line -->
<div class="absolute top-4 left-0 w-full h-1 bg-slate-200 -z-0"></div>
<!-- Active Line -->
<div class="absolute top-4 left-0 h-1 bg-green-500 -z-0 transition-all duration-500" :style="{ width: progressWidth }"></div>
<div class="flex justify-between relative z-10">
<!-- Step 1 -->
<div class="flex flex-col items-center gap-2 w-24 -ml-12"> <!-- Fixed width and negative margin to center on point -->
<div class="w-9 h-9 rounded-full bg-green-500 text-white flex items-center justify-center font-bold text-sm border-4 border-slate-50"><i class="pi pi-check"></i></div>
<div class="text-center">
<div class="text-sm font-bold text-slate-900">提交订单</div>
<div class="text-xs text-slate-500 mt-1">{{ order.createTime }}</div>
</div>
</div>
<!-- Step 2 -->
<div class="flex flex-col items-center gap-2 w-24">
<div class="w-9 h-9 rounded-full flex items-center justify-center font-bold text-sm border-4 border-slate-50 transition-colors" :class="order.payTime ? 'bg-green-500 text-white' : 'bg-slate-200 text-slate-500'"><i class="pi pi-wallet"></i></div>
<div class="text-center">
<div class="text-sm font-bold" :class="order.payTime ? 'text-slate-900' : 'text-slate-500'">付款成功</div>
<div class="text-xs text-slate-500 mt-1" v-if="order.payTime">{{ order.payTime }}</div>
</div>
</div>
<!-- Step 3 -->
<div class="flex flex-col items-center gap-2 w-24">
<div class="w-9 h-9 rounded-full flex items-center justify-center font-bold text-sm border-4 border-slate-50 transition-colors" :class="order.status === 'completed' ? 'bg-green-500 text-white' : 'bg-slate-200 text-slate-500'"><i class="pi pi-box"></i></div>
<div class="text-center">
<div class="text-sm font-bold" :class="order.status === 'completed' ? 'text-slate-900' : 'text-slate-500'">{{ order.isVirtual ? '自动发货' : '商家发货' }}</div>
</div>
</div>
<!-- Step 4 -->
<div class="flex flex-col items-center gap-2 w-24 -mr-12">
<div class="w-9 h-9 rounded-full flex items-center justify-center font-bold text-sm border-4 border-slate-50 transition-colors" :class="order.status === 'completed' ? 'bg-green-500 text-white' : 'bg-slate-200 text-slate-500'"><i class="pi pi-thumbs-up"></i></div>
<div class="text-center">
<div class="text-sm font-bold" :class="order.status === 'completed' ? 'text-slate-900' : 'text-slate-500'">交易完成</div>
</div>
</div>
</div>
</div>
</div>
<!-- Info Stack (Single Column Layout) -->
<div class="space-y-8">
<!-- 1. Product Info -->
<div class="border border-slate-200 rounded-lg overflow-hidden">
<div class="bg-slate-50 px-6 py-3 border-b border-slate-200 font-bold text-slate-900 text-lg">商品信息</div>
<div class="p-6 flex flex-col sm:flex-row gap-6">
<img :src="order.cover" class="w-32 h-20 object-cover rounded bg-slate-100 flex-shrink-0">
<div class="flex-1 min-w-0">
<h3 class="font-bold text-slate-900 text-xl mb-2">{{ order.title }}</h3>
<div class="text-base text-slate-500 mb-3">{{ order.sku || '默认规格' }}</div>
<div v-if="order.isVirtual" class="inline-flex items-center px-2.5 py-1 rounded text-sm font-medium bg-blue-50 text-blue-600">虚拟商品</div>
</div>
<div class="text-right sm:text-right text-left">
<div class="font-bold text-slate-900 text-xl">¥ {{ order.price }}</div>
<div class="text-slate-500 text-base mt-1">x {{ order.quantity }}</div>
</div>
</div>
<!-- Price Calculation -->
<div class="border-t border-slate-100 p-6 bg-slate-50/50">
<div class="flex flex-col gap-2 ml-auto max-w-sm">
<div class="flex justify-between text-base text-slate-600">
<span>商品总额</span>
<span>¥ {{ (order.price * order.quantity).toFixed(2) }}</span>
</div>
<div class="flex justify-between text-base text-slate-600">
<span>运费</span>
<span>¥ 0.00</span>
</div>
<div class="flex justify-between text-base text-slate-600">
<span>优惠券</span>
<span class="text-red-600">- ¥ 0.00</span>
</div>
<div class="border-t border-slate-200 my-2 pt-4 flex justify-between items-center">
<span class="font-bold text-slate-900 text-lg">实付金额</span>
<span class="text-3xl font-bold text-red-600">¥ {{ order.amount }}</span>
</div>
</div>
</div>
</div>
<!-- 2. Order Meta Info (Full Width) -->
<div class="bg-white border border-slate-200 rounded-xl p-8 text-base">
<div class="grid grid-cols-1 md:grid-cols-2 gap-12">
<!-- Left Column: Basic Info -->
<div class="space-y-6">
<div>
<h3 class="font-bold text-slate-900 text-lg mb-4 border-l-4 border-primary-500 pl-3">订单信息</h3>
<div class="space-y-3 text-slate-600">
<p>订单编号: <span class="text-slate-900 select-all">{{ order.id }}</span> <button class="text-primary-600 ml-2 hover:underline font-medium">复制</button></p>
<p>创建时间: {{ order.createTime }}</p>
<p v-if="order.payTime">付款时间: {{ order.payTime }}</p>
</div>
</div>
<div v-if="!order.isVirtual">
<h3 class="font-bold text-slate-900 text-lg mb-4 border-l-4 border-primary-500 pl-3">收货信息</h3>
<div class="space-y-2 text-slate-600">
<p class="font-bold text-slate-900 text-lg">张三 138****8888</p>
<p>北京市 朝阳区 建国路88号</p>
</div>
</div>
</div>
<!-- Right Column: Merchant & Actions -->
<div class="space-y-6">
<div>
<h3 class="font-bold text-slate-900 text-lg mb-4 border-l-4 border-primary-500 pl-3">商家信息</h3>
<div class="flex items-center gap-4 p-4 bg-slate-50 rounded-lg">
<img :src="order.tenantAvatar" class="w-12 h-12 rounded-full">
<div>
<div class="font-bold text-slate-900">{{ order.tenantName }}</div>
<button class="text-primary-600 text-sm mt-1 hover:underline">联系商家</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const orderId = route.params.id;
// Mock Data logic based on ID logic or just static for demo
const order = ref({
id: orderId || '82934712',
createTime: '2025-12-24 14:30:00',
payTime: '2025-12-24 14:30:05',
status: 'completed',
isVirtual: true,
cover: 'https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
title: '《霸王别姬》全本实录珍藏版',
sku: '高清数字版',
price: 9.90,
quantity: 1,
amount: '9.90',
tenantName: '梅派传人小林',
tenantAvatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Master1'
});
const progressWidth = computed(() => {
if (order.value.status === 'completed') return '100%';
if (order.value.status === 'paid') return '66%';
return '33%';
});
</script>

View File

@@ -0,0 +1,6 @@
<template>
<div class="mx-auto max-w-screen-xl my-8 p-8 bg-white rounded-xl shadow-sm">
<h1 class="text-2xl font-bold mb-4">Payment Cashier</h1>
<p class="text-slate-400">Order ID: {{ $route.params.id }}</p>
</div>
</template>

View File

@@ -0,0 +1,10 @@
<template>
<div>
<!-- Placeholder for Tenant Home View -->
<div class="bg-white p-8 rounded-xl shadow-sm border border-slate-100 mx-auto max-w-screen-xl my-8 text-center">
<h1 class="text-2xl font-bold mb-4">Tenant Home Page</h1>
<p class="text-slate-500">Tenant ID: {{ $route.params.id }}</p>
<p class="text-slate-400 mt-2">(Implementation pending based on PAGE_TENANT_HOME.md)</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,72 @@
<template>
<div class="mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8 py-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Stat Cards -->
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 flex items-center gap-4">
<div class="w-12 h-12 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center text-xl"><i class="pi pi-wallet"></i></div>
<div>
<div class="text-sm text-slate-500">账户余额</div>
<div class="text-2xl font-bold text-slate-900">¥ 128.50</div>
</div>
<!-- <button class="ml-auto text-sm font-medium text-primary-600 hover:bg-primary-50 px-3 py-1.5 rounded transition-colors">充值</button> -->
</div>
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 flex items-center gap-4">
<div class="w-12 h-12 rounded-full bg-yellow-50 text-yellow-600 flex items-center justify-center text-xl"><i class="pi pi-star"></i></div>
<div>
<div class="text-sm text-slate-500">我的积分</div>
<div class="text-2xl font-bold text-slate-900">2,450</div>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 flex items-center gap-4">
<div class="w-12 h-12 rounded-full bg-red-50 text-red-600 flex items-center justify-center text-xl"><i class="pi pi-ticket"></i></div>
<div>
<div class="text-sm text-slate-500">优惠券</div>
<div class="text-2xl font-bold text-slate-900">3 </div>
</div>
</div>
</div>
<!-- Recent Orders -->
<div class="mt-8 bg-white rounded-xl shadow-sm border border-slate-100 p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900">最近订单</h2>
<router-link to="/me/orders" class="text-sm text-primary-600 hover:text-primary-700 font-medium">查看全部 <i class="pi pi-angle-right"></i></router-link>
</div>
<div class="space-y-4">
<div class="flex items-center gap-4 p-4 border border-slate-100 rounded-lg hover:border-slate-300 transition-colors cursor-pointer">
<div class="w-16 h-16 bg-slate-100 rounded object-cover flex-shrink-0">
<img src="https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60" class="w-full h-full object-cover rounded">
</div>
<div class="flex-1 min-w-0">
<h3 class="font-bold text-slate-900 truncate">霸王别姬全本实录珍藏版</h3>
<div class="text-sm text-slate-500 mt-1">2025-12-24 14:30 · 订单号: 82934712</div>
</div>
<div class="text-right">
<div class="font-bold text-slate-900">¥ 9.90</div>
<div class="text-sm text-green-600 mt-1">交易成功</div>
</div>
</div>
<!-- More items... -->
</div>
</div>
<!-- Recent Views -->
<div class="mt-8 bg-white rounded-xl shadow-sm border border-slate-100 p-6">
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-slate-900">最近浏览</h2>
<button class="text-sm text-slate-500 hover:text-slate-700"><i class="pi pi-trash"></i> 清空历史</button>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<div v-for="i in 5" :key="i" class="group cursor-pointer">
<div class="aspect-[16/9] bg-slate-100 rounded-lg overflow-hidden mb-2 relative">
<img :src="`https://images.unsplash.com/photo-1469571486292-0ba58a3f068b?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=60`" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors"></div>
</div>
<h4 class="text-sm font-medium text-slate-800 line-clamp-2 group-hover:text-primary-600">京剧名家谈戏曲传承与创新发展的思考</h4>
<div class="text-xs text-slate-400 mt-1">10分钟前</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">My Library</h1>
<p class="text-slate-400">(Purchased content)</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">Notifications</h1>
<p class="text-slate-400">(Message center)</p>
</div>
</template>

View File

@@ -0,0 +1,165 @@
<template>
<div class="bg-white rounded-xl shadow-sm border border-slate-100 min-h-[600px]">
<!-- Header & Tabs -->
<div class="px-6 pt-6 border-b border-slate-100">
<h1 class="text-2xl font-bold text-slate-900 mb-6">我的订单</h1>
<div class="flex items-center gap-8">
<button
v-for="tab in tabs"
:key="tab.value"
@click="currentTab = tab.value"
class="pb-4 text-sm font-medium transition-colors border-b-2"
:class="currentTab === tab.value ? 'text-primary-600 border-primary-600' : 'text-slate-500 border-transparent hover:text-slate-700'"
>
{{ tab.label }}
</button>
</div>
</div>
<!-- Order List -->
<div class="p-6 space-y-6">
<!-- Search/Filter (Optional) -->
<div class="flex justify-end mb-4">
<div class="relative w-64">
<i class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
<input type="text" placeholder="搜索订单号或商品..." class="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-primary-500">
</div>
</div>
<!-- List Items -->
<div v-if="filteredOrders.length > 0" class="space-y-6">
<div v-for="order in filteredOrders" :key="order.id" class="border border-slate-200 rounded-lg overflow-hidden hover:border-slate-300 transition-colors">
<!-- Order Header -->
<div class="bg-slate-50 px-4 py-3 flex items-center justify-between text-sm text-slate-500">
<div class="flex items-center gap-4">
<span class="font-medium text-slate-900">{{ order.date }}</span>
<span>订单号: {{ order.id }}</span>
<span>{{ order.tenantName }}</span>
</div>
<div class="font-bold" :class="statusColor(order.status)">{{ statusText(order.status) }}</div>
</div>
<!-- Order Body -->
<div class="p-4 flex flex-col sm:flex-row gap-6">
<!-- Product Info -->
<div class="flex-1 flex gap-4">
<div class="w-24 h-16 bg-slate-100 rounded object-cover flex-shrink-0 relative overflow-hidden">
<img :src="order.cover" class="w-full h-full object-cover">
<div v-if="order.type === 'video'" class="absolute inset-0 flex items-center justify-center bg-black/20 text-white"><i class="pi pi-play-circle"></i></div>
</div>
<div>
<h3 class="font-bold text-slate-900 line-clamp-1 mb-1">{{ order.title }}</h3>
<div class="text-xs text-slate-500 mb-2">{{ order.typeLabel }}</div>
<div v-if="order.isVirtual" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 text-blue-600">虚拟发货</div>
</div>
</div>
<!-- Price & Actions -->
<div class="flex items-center justify-between sm:w-1/3 sm:border-l sm:border-slate-100 sm:pl-6">
<div class="text-right sm:text-left">
<div class="font-bold text-slate-900 text-lg">¥ {{ order.amount }}</div>
<div class="text-xs text-slate-400">在线支付</div>
</div>
<div class="flex flex-col gap-2">
<button v-if="order.status === 'unpaid'" class="px-4 py-1.5 bg-primary-600 text-white text-sm font-medium rounded-lg hover:bg-primary-700">去支付</button>
<router-link :to="`/me/orders/${order.id}`" v-if="order.status === 'paid' || order.status === 'completed'" class="px-4 py-1.5 border border-slate-300 text-slate-700 text-sm font-medium rounded-lg hover:bg-slate-50 inline-block text-center">查看详情</router-link>
<button v-if="order.status === 'completed'" class="px-4 py-1.5 text-primary-600 text-sm hover:underline">申请售后</button>
<button v-if="order.status === 'unpaid'" class="text-xs text-slate-400 hover:text-slate-600">取消订单</button>
</div>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="text-center py-20">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-slate-50 mb-4">
<i class="pi pi-shopping-bag text-2xl text-slate-300"></i>
</div>
<p class="text-slate-500 text-lg">暂无相关订单</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const currentTab = ref('all');
const tabs = [
{ label: '全部订单', value: 'all' },
{ label: '待支付', value: 'unpaid' },
{ label: '已完成', value: 'completed' },
{ label: '退款/售后', value: 'refund' }
];
// Mock Data
const orders = ref([
{
id: '82934712',
date: '2025-12-24 14:30',
tenantName: '梅派传人小林',
cover: 'https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
title: '《霸王别姬》全本实录珍藏版',
type: 'video',
typeLabel: '戏曲视频',
isVirtual: true,
amount: '9.90',
status: 'completed'
},
{
id: '82934713',
date: '2025-12-23 09:15',
tenantName: '戏曲周边商城',
cover: 'https://images.unsplash.com/photo-1557683316-973673baf926?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
title: '京剧脸谱纪念书签 (一套4张)',
type: 'product',
typeLabel: '实体商品',
isVirtual: false,
amount: '45.00',
status: 'unpaid'
},
{
id: '82934711',
date: '2025-12-20 18:20',
tenantName: '豫剧李大师',
cover: 'https://images.unsplash.com/photo-1469571486292-0ba58a3f068b?ixlib=rb-1.2.1&auto=format&fit=crop&w=100&q=60',
title: '豫剧唱腔发音技巧专栏',
type: 'article',
typeLabel: '付费专栏',
isVirtual: true,
amount: '99.00',
status: 'refunded' // or refunding
}
]);
const filteredOrders = computed(() => {
if (currentTab.value === 'all') return orders.value;
if (currentTab.value === 'refund') return orders.value.filter(o => ['refunded', 'refunding'].includes(o.status));
return orders.value.filter(o => o.status === currentTab.value);
});
const statusText = (status) => {
const map = {
unpaid: '待支付',
paid: '已支付',
completed: '交易成功',
refunding: '退款中',
refunded: '已退款',
cancelled: '已取消'
};
return map[status] || status;
};
const statusColor = (status) => {
const map = {
unpaid: 'text-orange-600',
paid: 'text-blue-600',
completed: 'text-green-600',
refunding: 'text-purple-600',
refunded: 'text-slate-500',
cancelled: 'text-slate-400'
};
return map[status] || 'text-slate-500';
};
</script>

View File

@@ -0,0 +1,6 @@
<template>
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">Profile Settings</h1>
<p class="text-slate-400">(Edit profile)</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">Account Security</h1>
<p class="text-slate-400">(Password and bindings)</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">My Wallet</h1>
<p class="text-slate-400">(Balance and transactions)</p>
</div>
</template>

View File

@@ -0,0 +1,22 @@
import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
tailwindcss(),
],
server: {
host: '0.0.0.0',
port: 5174,
strictPort: true,
proxy: {
'/v1': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
},
})