consumer模块完成90%,前端完成supabase对接
This commit is contained in:
115
mall/pages/SQL_FILES_CLEANUP_SUMMARY.md
Normal file
115
mall/pages/SQL_FILES_CLEANUP_SUMMARY.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# SQL 文件整理完成
|
||||
|
||||
## ✅ 已完成的整理
|
||||
|
||||
### 1. 移除重复的简化表定义
|
||||
- ✅ 从 `ANALYTICS_DB_SCHEMA.sql` 中移除了简化的 `user_sessions` 和 `page_views` 定义
|
||||
- ✅ 添加了注释说明依赖关系
|
||||
|
||||
### 2. 添加依赖说明
|
||||
- ✅ 在 `01_create_tables.sql` 中添加了注释,说明可能与 `USER_AUTH_SCHEMA.sql` 重复
|
||||
- ✅ 在 `USER_AUTH_SCHEMA.sql` 中添加了注释,说明可能与 `01_create_tables.sql` 重复
|
||||
|
||||
---
|
||||
|
||||
## 📋 当前文件结构
|
||||
|
||||
### `pages/user/test/` - 用户认证相关
|
||||
1. **`USER_AUTH_SCHEMA.sql`** ⭐
|
||||
- `ak_users` 表(业务用户资料)
|
||||
- `users` 表(统计用,可能与 analytics 重复)
|
||||
- `user_sessions` 表(会话统计,可能与 analytics 重复)
|
||||
- `upsert_user_profile` RPC 函数
|
||||
- `handle_new_user` 触发器函数(注释中)
|
||||
|
||||
2. **`USER_AUTH_TRIGGER.sql`** ⭐
|
||||
- `on_auth_user_created` 触发器(在 auth.users 插入时自动创建 ak_users)
|
||||
|
||||
3. **`USER_AUTH_TEST_DATA.sql`**(可选)
|
||||
- 测试数据
|
||||
|
||||
### `pages/mall/analytics/test/` - 数据分析相关
|
||||
1. **`01_create_tables.sql`** ⭐
|
||||
- 业务核心表:`orders`, `order_items`, `products`, `merchants`
|
||||
- 统计表:`users`, `user_sessions`, `page_views`(可能与 USER_AUTH_SCHEMA.sql 重复)
|
||||
- RLS 策略
|
||||
- `update_updated_at_column` 函数和触发器
|
||||
|
||||
2. **`ANALYTICS_DB_SCHEMA.sql`** ⭐
|
||||
- 分析表:`analytics_*` 系列表
|
||||
- RPC 函数(用于数据分析)
|
||||
- **已移除**:简化的 `user_sessions` 和 `page_views` 定义
|
||||
|
||||
3. **`02_insert_test_data.sql`**(可选)
|
||||
- 业务表测试数据
|
||||
|
||||
4. **`ANALYTICS_TEST_SEED.sql`**(可选)
|
||||
- 分析表测试数据
|
||||
|
||||
5. **`03_test_queries.sql`**(可选)
|
||||
- 测试查询
|
||||
|
||||
6. **`04_cleanup.sql`**(可选)
|
||||
- 清理脚本
|
||||
|
||||
---
|
||||
|
||||
## 🚀 推荐执行顺序
|
||||
|
||||
### 首次部署
|
||||
```sql
|
||||
-- 1. 用户认证表(包含 users, user_sessions)
|
||||
pages/user/test/USER_AUTH_SCHEMA.sql
|
||||
pages/user/test/USER_AUTH_TRIGGER.sql
|
||||
|
||||
-- 2. 业务表(会跳过已存在的 users, user_sessions)
|
||||
pages/mall/analytics/test/01_create_tables.sql
|
||||
|
||||
-- 3. 分析表(依赖业务表)
|
||||
pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql
|
||||
|
||||
-- 4. 测试数据(可选)
|
||||
pages/mall/analytics/test/02_insert_test_data.sql
|
||||
pages/mall/analytics/test/ANALYTICS_TEST_SEED.sql
|
||||
```
|
||||
|
||||
### 后续更新
|
||||
- 如果只更新分析表,只需执行 `ANALYTICS_DB_SCHEMA.sql`
|
||||
- 如果只更新业务表,只需执行 `01_create_tables.sql`
|
||||
- 如果只更新用户认证,只需执行 `USER_AUTH_SCHEMA.sql`
|
||||
|
||||
---
|
||||
|
||||
## 🔍 重复内容说明
|
||||
|
||||
### 已处理的重复
|
||||
1. ✅ **`user_sessions` 表** - 保留在 `USER_AUTH_SCHEMA.sql` 和 `01_create_tables.sql` 中的完整定义,移除 `ANALYTICS_DB_SCHEMA.sql` 中的简化定义
|
||||
2. ✅ **`page_views` 表** - 保留在 `01_create_tables.sql` 中的完整定义,移除 `ANALYTICS_DB_SCHEMA.sql` 中的简化定义
|
||||
|
||||
### 保留的重复(安全)
|
||||
1. **`users` 表** - 在 `USER_AUTH_SCHEMA.sql` 和 `01_create_tables.sql` 中都有定义,使用 `IF NOT EXISTS` 不会冲突
|
||||
2. **`update_updated_at_column` 函数** - 在多个文件中定义,使用 `CREATE OR REPLACE FUNCTION` 不会冲突
|
||||
3. **触发器** - 使用 `IF NOT EXISTS` 或 `DROP TRIGGER IF EXISTS` 确保不会冲突
|
||||
|
||||
---
|
||||
|
||||
## ✅ 验证
|
||||
|
||||
执行以下查询验证表结构:
|
||||
```sql
|
||||
-- 检查 user_sessions 表字段(应该是完整定义)
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'user_sessions' AND table_schema = 'public'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- 检查 page_views 表字段(应该是完整定义)
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'page_views' AND table_schema = 'public'
|
||||
ORDER BY ordinal_position;
|
||||
```
|
||||
|
||||
**预期结果**:
|
||||
- `user_sessions` 应包含:id, user_id, session_token, last_active_at, is_active, ip_address, user_agent, created_at, updated_at
|
||||
- `page_views` 应包含:id, user_id, path, source, referrer, ip_address, user_agent, created_at
|
||||
119
mall/pages/SQL_FILES_ORGANIZATION.md
Normal file
119
mall/pages/SQL_FILES_ORGANIZATION.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# SQL 文件整理说明
|
||||
|
||||
## 📋 重复内容分析
|
||||
|
||||
经过检查,发现以下重复定义:
|
||||
|
||||
### 1. **`users` 表**(重复)
|
||||
- ✅ `pages/user/test/USER_AUTH_SCHEMA.sql` (第 63-71 行)
|
||||
- ✅ `pages/mall/analytics/test/01_create_tables.sql` (第 43-51 行)
|
||||
- **状态**:两个定义相同,使用 `CREATE TABLE IF NOT EXISTS` 不会冲突,但建议统一
|
||||
|
||||
### 2. **`user_sessions` 表**(重复,定义略有不同)
|
||||
- ✅ `pages/user/test/USER_AUTH_SCHEMA.sql` (第 76-86 行) - **完整定义**(推荐)
|
||||
- ✅ `pages/mall/analytics/test/01_create_tables.sql` (第 24-34 行) - **完整定义**(相同)
|
||||
- ⚠️ `pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql` (第 19-25 行) - **简化定义**(字段较少)
|
||||
|
||||
### 3. **`page_views` 表**(重复,定义不同)
|
||||
- ✅ `pages/mall/analytics/test/01_create_tables.sql` (第 90-99 行) - **完整定义**(推荐)
|
||||
- ⚠️ `pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql` (第 30-36 行) - **简化定义**(字段较少)
|
||||
|
||||
### 4. **`update_updated_at_column` 函数**(重复)
|
||||
- ✅ `pages/user/test/USER_AUTH_SCHEMA.sql` (第 93-99 行)
|
||||
- ✅ `pages/mall/analytics/test/01_create_tables.sql` (第 107-113 行)
|
||||
- **状态**:两个定义相同,使用 `CREATE OR REPLACE FUNCTION` 不会冲突
|
||||
|
||||
### 5. **触发器**(部分重复)
|
||||
- `USER_AUTH_SCHEMA.sql`: `update_users_updated_at`, `update_user_sessions_updated_at`
|
||||
- `01_create_tables.sql`: `update_orders_updated_at`, `update_user_sessions_updated_at`, `update_users_updated_at`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 整理方案
|
||||
|
||||
### 方案一:保持现状(推荐)
|
||||
**优点**:每个文件独立,使用 `IF NOT EXISTS` 和 `CREATE OR REPLACE` 不会冲突
|
||||
**缺点**:有重复代码
|
||||
|
||||
**执行顺序**:
|
||||
1. `pages/user/test/USER_AUTH_SCHEMA.sql` - 创建用户认证相关表
|
||||
2. `pages/mall/analytics/test/01_create_tables.sql` - 创建业务表(会跳过已存在的表)
|
||||
3. `pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql` - 创建分析表(会跳过已存在的表)
|
||||
|
||||
### 方案二:统一到基础表文件(更清晰)
|
||||
**优点**:减少重复,职责清晰
|
||||
**缺点**:需要重构文件结构
|
||||
|
||||
**建议结构**:
|
||||
- `00_base_tables.sql` - 基础表(users, user_sessions, page_views)
|
||||
- `01_user_auth.sql` - 用户认证表(ak_users)和函数
|
||||
- `02_business_tables.sql` - 业务表(orders, products, merchants等)
|
||||
- `03_analytics_tables.sql` - 分析表(analytics_*)
|
||||
|
||||
---
|
||||
|
||||
## 📝 当前文件职责
|
||||
|
||||
### `pages/user/test/` 目录
|
||||
- **`USER_AUTH_SCHEMA.sql`** - 用户认证核心表(ak_users, users, user_sessions)和 RPC 函数
|
||||
- **`USER_AUTH_TRIGGER.sql`** - 数据库触发器(自动创建 ak_users)
|
||||
- **`USER_AUTH_TEST_DATA.sql`** - 测试数据
|
||||
|
||||
### `pages/mall/analytics/test/` 目录
|
||||
- **`01_create_tables.sql`** - 业务表(orders, users, user_sessions, products, merchants, order_items, page_views)+ RLS
|
||||
- **`ANALYTICS_DB_SCHEMA.sql`** - 分析表(analytics_*)+ RPC 函数
|
||||
- **`02_insert_test_data.sql`** - 业务表测试数据
|
||||
- **`ANALYTICS_TEST_SEED.sql`** - 分析表测试数据
|
||||
- **`03_test_queries.sql`** - 测试查询
|
||||
- **`04_cleanup.sql`** - 清理脚本
|
||||
|
||||
---
|
||||
|
||||
## ✅ 推荐操作
|
||||
|
||||
### 立即执行(保持现状)
|
||||
当前文件结构可以使用,因为:
|
||||
1. 所有表使用 `CREATE TABLE IF NOT EXISTS`
|
||||
2. 所有函数使用 `CREATE OR REPLACE FUNCTION`
|
||||
3. 触发器使用 `CREATE TRIGGER IF NOT EXISTS` 或 `DROP TRIGGER IF EXISTS`
|
||||
|
||||
**执行顺序**:
|
||||
```sql
|
||||
-- 1. 用户认证表
|
||||
pages/user/test/USER_AUTH_SCHEMA.sql
|
||||
pages/user/test/USER_AUTH_TRIGGER.sql
|
||||
|
||||
-- 2. 业务表(会跳过已存在的 users, user_sessions)
|
||||
pages/mall/analytics/test/01_create_tables.sql
|
||||
|
||||
-- 3. 分析表(会跳过已存在的 user_sessions, page_views)
|
||||
pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql
|
||||
```
|
||||
|
||||
### 未来优化(可选)
|
||||
如果需要减少重复,可以:
|
||||
1. 从 `ANALYTICS_DB_SCHEMA.sql` 中移除 `user_sessions` 和 `page_views` 的简化定义
|
||||
2. 确保 `01_create_tables.sql` 先执行,提供完整定义
|
||||
3. 在 `ANALYTICS_DB_SCHEMA.sql` 中添加注释说明依赖关系
|
||||
|
||||
---
|
||||
|
||||
## 🔍 验证重复
|
||||
|
||||
执行以下查询检查表是否存在:
|
||||
```sql
|
||||
-- 检查 users 表
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'users' AND table_schema = 'public';
|
||||
|
||||
-- 检查 user_sessions 表
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'user_sessions' AND table_schema = 'public';
|
||||
|
||||
-- 检查 page_views 表
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'page_views' AND table_schema = 'public';
|
||||
```
|
||||
345
mall/pages/dashboard/OrderChart.uvue
Normal file
345
mall/pages/dashboard/OrderChart.uvue
Normal file
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<view class="order-chart-container">
|
||||
<view class="chart-header">
|
||||
<view class="header-left">
|
||||
<view class="title-icon">
|
||||
<text class="iconfont">Icon</text>
|
||||
</view>
|
||||
<text class="chart-title">订单分析</text>
|
||||
</view>
|
||||
|
||||
<view class="chart-controls">
|
||||
<view
|
||||
v-for="p in periods"
|
||||
:key="p.value"
|
||||
class="seg-btn"
|
||||
:class="{ active: range === p.value }"
|
||||
@click="changeRange(p.value)"
|
||||
>
|
||||
<text class="seg-btn-text">{{ p.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="chart-body">
|
||||
<!-- 轴标题 -->
|
||||
<view class="axis-titles">
|
||||
<text class="axis-title">金额 (元)</text>
|
||||
<text class="axis-title">数量 (笔)</text>
|
||||
</view>
|
||||
|
||||
<!-- 图表容器 -->
|
||||
<view class="chart-view-wrapper">
|
||||
<EChartsView
|
||||
v-if="!loading && chartOption"
|
||||
:option="chartOption"
|
||||
class="echarts-view"
|
||||
/>
|
||||
|
||||
<!-- Loading 状态 -->
|
||||
<view v-if="loading" class="loading-state">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="!loading && stats.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import { OrderStat } from '@/types/orders.uts'
|
||||
|
||||
// --- Props ---
|
||||
const props = defineProps<{
|
||||
// 可以在这里扩展外部传入的配置
|
||||
}>()
|
||||
|
||||
// --- State ---
|
||||
const range = ref('30d')
|
||||
const loading = ref(false)
|
||||
const stats = ref<OrderStat[]>([])
|
||||
|
||||
const periods = [
|
||||
{ label: '30天', value: '30d' },
|
||||
{ label: '周', value: 'week' },
|
||||
{ label: '月', value: 'month' },
|
||||
{ label: '年', value: 'year' }
|
||||
]
|
||||
|
||||
// --- ECharts Options ---
|
||||
const chartOption = computed(() => {
|
||||
if (stats.value.length === 0) return null
|
||||
|
||||
const dates = stats.value.map(item => item.date)
|
||||
const amounts = stats.value.map(item => item.totalAmount)
|
||||
const counts = stats.value.map(item => item.orderCount)
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
crossStyle: { color: '#999' }
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '3%',
|
||||
bottom: '3%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
legend: {
|
||||
data: ['订单金额', '订单数'],
|
||||
top: 0,
|
||||
itemWidth: 12,
|
||||
itemHeight: 12
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisPointer: { type: 'shadow' },
|
||||
axisLine: { lineStyle: { color: '#f0f0f0' } },
|
||||
axisLabel: { color: '#999', fontSize: 10 }
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '', // 已经在外部显示标题了
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#f0f0f0' } },
|
||||
axisLabel: { color: '#999' }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '',
|
||||
splitLine: { show: false },
|
||||
axisLabel: { color: '#999' }
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '订单金额',
|
||||
type: 'bar',
|
||||
barWidth: '40%',
|
||||
data: amounts,
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: '#36a3f7' },
|
||||
{ offset: 1, color: '#a0d4fb' }
|
||||
]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '订单数',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: counts,
|
||||
smooth: true,
|
||||
showSymbol: true,
|
||||
symbolSize: 6,
|
||||
itemStyle: { color: '#2fc25b' },
|
||||
lineStyle: { width: 2, color: '#2fc25b' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(47, 194, 91, 0.2)' },
|
||||
{ offset: 1, color: 'rgba(47, 194, 91, 0)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// --- Methods ---
|
||||
|
||||
/**
|
||||
* 内部刷新逻辑,供外部或生命周期调用
|
||||
*/
|
||||
const refresh = async () => {
|
||||
await fetchOrderStats(range.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟接口请求
|
||||
*/
|
||||
const fetchOrderStats = (r: string) => {
|
||||
loading.value = true
|
||||
|
||||
// 模拟网络延迟
|
||||
setTimeout(() => {
|
||||
// 模拟数据生成逻辑
|
||||
const mockData: OrderStat[] = []
|
||||
const count = r === 'week' ? 7 : (r === 'month' ? 30 : (r === 'year' ? 12 : 30))
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
mockData.push({
|
||||
date: r === 'year' ? `${i + 1}月` : `01-${(i + 4).toString().padStart(2, '0')}`,
|
||||
totalAmount: Math.floor(Math.random() * 80000) + (i === 4 ? 60000 : 20000),
|
||||
orderCount: Math.floor(Math.random() * 25) + (i % 3 === 0 ? 8 : 2)
|
||||
})
|
||||
}
|
||||
|
||||
stats.value = mockData
|
||||
loading.value = false
|
||||
// 如果是下拉刷新触发,通知原生停止
|
||||
uni.stopPullDownRefresh()
|
||||
}, 800)
|
||||
}
|
||||
|
||||
const changeRange = (v: string) => {
|
||||
range.value = v
|
||||
}
|
||||
|
||||
// 暴露给父组件的主动刷新方案
|
||||
defineExpose({
|
||||
refresh
|
||||
})
|
||||
|
||||
// --- Lifecycle & Watchers ---
|
||||
onMounted(() => {
|
||||
fetchOrderStats(range.value)
|
||||
})
|
||||
|
||||
watch(range, (newVal) => {
|
||||
fetchOrderStats(newVal)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.order-chart-container {
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #e6f7ff;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 分段按钮样式 */
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.seg-btn {
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.seg-btn:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.seg-btn.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.seg-btn-text {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.seg-btn.active .seg-btn-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 图表内容区域 */
|
||||
.chart-body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.axis-titles {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.axis-title {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.chart-view-wrapper {
|
||||
width: 100%;
|
||||
height: 350px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.echarts-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-state, .empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-text, .empty-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
207
mall/pages/dashboard/PurchaseUserPie.uvue
Normal file
207
mall/pages/dashboard/PurchaseUserPie.uvue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<view class="chart-container">
|
||||
<view class="chart-header">
|
||||
<view class="header-left">
|
||||
<view class="title-icon">
|
||||
<text class="iconfont">O</text>
|
||||
</view>
|
||||
<text class="chart-title">购买用户分析</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="chart-body">
|
||||
<view class="chart-view-wrapper">
|
||||
<EChartsView
|
||||
v-if="!loading && chartOption"
|
||||
:option="chartOption"
|
||||
class="echarts-view"
|
||||
/>
|
||||
|
||||
<!-- Loading 状态 -->
|
||||
<view v-if="loading" class="loading-state">
|
||||
<text class="loading-text">计算中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空数据状态 -->
|
||||
<view v-if="!loading && chartData.length === 0" class="empty-state">
|
||||
<text class="empty-text">无相关消费记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import { PurchaseUserStat } from '@/types/charts.uts'
|
||||
|
||||
/**
|
||||
* PurchaseUserPie 购买用户统计饼图
|
||||
* 展示不同消费特征的用户分布情况
|
||||
*/
|
||||
|
||||
// --- 状态定义 ---
|
||||
const loading = ref(false)
|
||||
const chartData = ref<PurchaseUserStat[]>([])
|
||||
|
||||
// --- ECharts 配置计算属性 ---
|
||||
const chartOption = computed(() : any => {
|
||||
if (chartData.value.length === 0) return null
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
bottom: '5%',
|
||||
left: 'center',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#666'
|
||||
}
|
||||
},
|
||||
// 视觉色彩方案
|
||||
color: ['#1890ff', '#36cfc9', '#ffc53d', '#ff4d4f'],
|
||||
series: [
|
||||
{
|
||||
name: '用户分布',
|
||||
type: 'pie',
|
||||
radius: ['45%', '70%'], // 采用环形图设计,更具现代感
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 6,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: '{b}\n{d}%',
|
||||
fontSize: 11,
|
||||
color: '#888'
|
||||
},
|
||||
emphasis: {
|
||||
scale: true,
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
length: 12,
|
||||
length2: 10,
|
||||
smooth: true
|
||||
},
|
||||
data: chartData.value.map(item => {
|
||||
return {
|
||||
name: item.label,
|
||||
value: item.value
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// --- 数据获取逻辑 ---
|
||||
|
||||
/**
|
||||
* 模拟获取统计数据
|
||||
*/
|
||||
const fetchData = () => {
|
||||
loading.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
chartData.value = [
|
||||
{ label: '未消费用户', value: 342 } as PurchaseUserStat,
|
||||
{ label: '消费一次', value: 156 } as PurchaseUserStat,
|
||||
{ label: '留存客户', value: 218 } as PurchaseUserStat,
|
||||
{ label: '回流客户', value: 84 } as PurchaseUserStat
|
||||
]
|
||||
loading.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
refresh: fetchData
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart-container {
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #fff7e6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chart-body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-view-wrapper {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.echarts-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-state, .empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-text, .empty-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
370
mall/pages/dashboard/UserTrendChart.uvue
Normal file
370
mall/pages/dashboard/UserTrendChart.uvue
Normal file
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<view class="chart-container">
|
||||
<!-- 图表标题与切换 -->
|
||||
<view class="chart-header">
|
||||
<view class="header-left">
|
||||
<view class="title-icon">
|
||||
<text class="iconfont">≡</text>
|
||||
</view>
|
||||
<text class="chart-title">用户增长趋势</text>
|
||||
</view>
|
||||
|
||||
<view class="chart-controls">
|
||||
<view
|
||||
v-for="p in periods"
|
||||
:key="p.value"
|
||||
class="seg-btn"
|
||||
:class="{ active: range === p.value }"
|
||||
@click="changeRange(p.value)"
|
||||
>
|
||||
<text class="seg-btn-text">{{ p.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图表内容 -->
|
||||
<view class="chart-body">
|
||||
<view class="axis-titles">
|
||||
<text class="axis-title">新增用户 (人)</text>
|
||||
</view>
|
||||
|
||||
<view class="chart-view-wrapper">
|
||||
<EChartsView
|
||||
v-if="!loading && chartOption"
|
||||
:option="chartOption"
|
||||
class="echarts-view"
|
||||
/>
|
||||
|
||||
<!-- Loading 状态 -->
|
||||
<view v-if="loading" class="loading-state">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空数据状态 -->
|
||||
<view v-if="!loading && trendData.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无趋势数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import { UserTrend } from '@/types/charts.uts'
|
||||
|
||||
/**
|
||||
* UserTrendChart 用户趋势图
|
||||
* 展示日粒度用户增长,采用平滑曲线面积图
|
||||
*/
|
||||
|
||||
// --- 状态定义 ---
|
||||
const loading = ref(false)
|
||||
const range = ref('7d')
|
||||
const trendData = ref<UserTrend[]>([])
|
||||
|
||||
const periods = [
|
||||
{ label: '7天', value: '7d' },
|
||||
{ label: '30天', value: '30d' }
|
||||
]
|
||||
|
||||
// --- ECharts 配置计算属性 ---
|
||||
const chartOption = computed(() : any => {
|
||||
if (trendData.value.length === 0) return null
|
||||
|
||||
const xData = trendData.value.map(item => item.date)
|
||||
const yData = trendData.value.map(item => item.count)
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
lineStyle: { color: '#1890ff', type: 'dashed' }
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
top: '5%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: xData,
|
||||
axisLine: { lineStyle: { color: '#f0f0f0' } },
|
||||
axisLabel: {
|
||||
color: '#999',
|
||||
fontSize: 10,
|
||||
rotate: xData.length > 7 ? 45 : 0
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#f5f5f5' } },
|
||||
axisLabel: { color: '#999', fontSize: 10 }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '新增用户',
|
||||
type: 'line',
|
||||
data: yData,
|
||||
smooth: true, // 平滑折线
|
||||
showSymbol: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
itemStyle: {
|
||||
color: '#1890ff',
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: '#1890ff'
|
||||
},
|
||||
// 区域填充效果
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(24, 144, 255, 0.25)' },
|
||||
{ offset: 1, color: 'rgba(24, 144, 255, 0)' }
|
||||
]
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'series'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// --- 方法定义 ---
|
||||
|
||||
const changeRange = (v: string) => {
|
||||
range.value = v
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟接口请求
|
||||
* 使用 useRequest 模式的封装
|
||||
*/
|
||||
const fetchData = (r: string) => {
|
||||
loading.value = true
|
||||
|
||||
// 模拟业务逻辑
|
||||
setTimeout(() => {
|
||||
const mockData: UserTrend[] = []
|
||||
const count = r === '7d' ? 7 : 30
|
||||
const now = new Date()
|
||||
|
||||
for (let i = count - 1; i >= 0; i--) {
|
||||
const d = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
|
||||
mockData.push({
|
||||
date: `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`,
|
||||
count: Math.floor(Math.random() * 80) + (i % 7 === 0 ? 50 : 20)
|
||||
} as UserTrend)
|
||||
}
|
||||
|
||||
trendData.value = mockData
|
||||
loading.value = false
|
||||
}, 800)
|
||||
}
|
||||
|
||||
// 暴露方法给外部调用
|
||||
defineExpose({
|
||||
refresh: () => fetchData(range.value)
|
||||
})
|
||||
|
||||
// --- 生命周期与监听 ---
|
||||
onMounted(() => {
|
||||
fetchData(range.value)
|
||||
})
|
||||
|
||||
watch(range, (newVal) => {
|
||||
fetchData(newVal)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart-container {
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #e6f7ff;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.seg-btn {
|
||||
height: 26px;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.seg-btn:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.seg-btn.active {
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.seg-btn-text {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.seg-btn.active .seg-btn-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.chart-body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.axis-titles {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.axis-title {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.chart-view-wrapper {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.echarts-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-state, .empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-text, .empty-text {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
// --- Data Fetching ---
|
||||
const fetchTrendData = () => {
|
||||
loading.value = true
|
||||
// 模拟 API 请求: GET /api/user/trend
|
||||
setTimeout(() => {
|
||||
const mock: UserTrend[] = []
|
||||
const now = new Date()
|
||||
for (let i = 14; i >= 0; i--) {
|
||||
const d = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
|
||||
mock.push({
|
||||
date: `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`,
|
||||
count: Math.floor(Math.random() * 50) + 20 + (i === 5 ? 100 : 0)
|
||||
})
|
||||
}
|
||||
trendData.value = mock
|
||||
loading.value = false
|
||||
}, 600)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTrendData()
|
||||
})
|
||||
|
||||
// 暴露刷新接口
|
||||
defineExpose({
|
||||
refresh: fetchTrendData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.echarts-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-overlay, .empty-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,255,255,0.6);
|
||||
}
|
||||
|
||||
.loading-text, .empty-text {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
429
mall/pages/info/README.md
Normal file
429
mall/pages/info/README.md
Normal file
@@ -0,0 +1,429 @@
|
||||
# 多语言AI资讯系统 (Multilingual AI Information System)
|
||||
|
||||
基于 uni-app-x (UTS Android) 平台开发的多语言AI资讯系统,严格遵循 UTS Android 开发规范。
|
||||
|
||||
## 系统特性
|
||||
|
||||
### 核心功能
|
||||
- **多语言支持**: 支持中文、英文、日文、韩文等多种语言
|
||||
- **AI助手**: 集成智能聊天助手,提供内容推荐和分析
|
||||
- **智能搜索**: 支持关键词搜索、分类筛选、高级过滤
|
||||
- **响应式设计**: 兼容大屏和小屏设备,自适应布局
|
||||
- ⚡ **高性能**: 基于 UTSJSONObject 优化,确保流畅体验
|
||||
- **数据分析**: 内容质量评分、用户行为分析
|
||||
|
||||
### 技术特点
|
||||
- 严格遵循 UTS Android 开发规范
|
||||
- 全部使用 UTSJSONObject 作为核心数据结构
|
||||
- 与 Supabase 数据库深度集成
|
||||
- 支持实时数据同步和离线缓存
|
||||
- 模块化设计,易于扩展和维护
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
pages/info/
|
||||
├── index.uvue # 主页面 - 资讯列表、分类导航、精选推荐
|
||||
├── detail.uvue # 详情页面 - 内容展示、多语言切换、相关推荐、评论互动
|
||||
├── search.uvue # 搜索页面 - 智能搜索、筛选、历史记录
|
||||
├── chat.uvue # AI助手页面 - 智能对话、内容推荐
|
||||
├── settings.uvue # 设置页面 - 个人偏好、语言设置、通知配置
|
||||
├── topics.uvue # 专题列表页面 - 专题分类、精选专题、热门专题
|
||||
├── topic-detail.uvue # 专题详情页面 - 专题内容、时间轴、相关推荐、评论互动
|
||||
├── comments.uvue # 评论组件 - 多层级评论、回复、点赞、举报功能
|
||||
├── types.uts # 类型定义 - 所有数据结构和工具函数
|
||||
├── test-utils.uts # 测试工具 - 功能验证和性能测试
|
||||
├── index-backup.uvue # 原始备份文件
|
||||
└── README.md # 项目文档
|
||||
```
|
||||
|
||||
## 页面功能详解
|
||||
|
||||
### 1. 主页面 (index.uvue)
|
||||
- **分类导航**: 水平滚动的分类标签,支持全部/各分类切换
|
||||
- **内容列表**: 瀑布流式布局,展示标题、摘要、作者等信息
|
||||
- **精选推荐**: 高质量内容推荐区域
|
||||
- **语言选择**: 弹窗式语言选择器
|
||||
- **实时统计**: 显示总内容数、发布数、趋势等统计信息
|
||||
- **响应式布局**: 大屏2列,小屏1列自适应
|
||||
|
||||
### 2. 详情页面 (detail.uvue)
|
||||
- **内容展示**: 支持原文和多语言翻译切换
|
||||
- **质量指标**: 显示内容质量评分和可视化指示器
|
||||
- **交互功能**: 点赞、分享、收藏等社交功能
|
||||
- **相关推荐**: 基于内容相似度的智能推荐
|
||||
- **标签导航**: 点击标签跳转到相关搜索
|
||||
- **AI助手入口**: 浮动按钮快速访问AI助手
|
||||
|
||||
### 3. 搜索页面 (search.uvue)
|
||||
- **智能搜索**: 支持标题、内容、标签等多维度搜索
|
||||
- **高级筛选**: 分类、语言、质量、时间等多条件筛选
|
||||
- **搜索建议**: 实时搜索建议和自动补全
|
||||
- **历史记录**: 搜索历史管理和快速重用
|
||||
- **热门搜索**: 展示热门搜索词和趋势
|
||||
- **结果排序**: 支持相关度、时间、质量等多种排序
|
||||
|
||||
### 4. AI助手页面 (chat.uvue)
|
||||
- **智能对话**: 支持自然语言交互
|
||||
- **内容推荐**: 基于用户偏好的个性化推荐
|
||||
- **快速操作**: 预设常用问题和快捷操作
|
||||
- **会话管理**: 支持多会话和历史记录
|
||||
- **消息操作**: 复制、点赞、反馈等消息交互
|
||||
- **上下文理解**: 结合当前浏览内容提供相关建议
|
||||
|
||||
### 5. 设置页面 (settings.uvue)
|
||||
- **语言偏好**: 界面语言和内容语言偏好设置
|
||||
- **内容偏好**: 感兴趣的分类、阅读模式、字体大小
|
||||
- **通知设置**: 推送通知、提醒频率等配置
|
||||
- **隐私安全**: 数据使用、隐私保护相关设置
|
||||
- **账户管理**: 个人信息编辑和账户设置
|
||||
- **应用信息**: 版本信息、用户协议、反馈渠道
|
||||
|
||||
### 6. 专题列表页面 (topics.uvue)
|
||||
- **专题分类**: 按类型筛选专题(突发事件、热门话题、系列专题等)
|
||||
- **精选专题**: 编辑推荐的高质量专题展示
|
||||
- **专题预览**: 专题封面、标题、描述、内容数量等信息
|
||||
- **排序筛选**: 支持按更新时间、热门度、内容数量等排序
|
||||
- **快速导航**: 一键跳转到专题详情或搜索页面
|
||||
|
||||
### 7. 专题详情页面 (topic-detail.uvue)
|
||||
- **专题概览**: 专题标题、描述、统计信息、封面展示
|
||||
- **内容组织**: 提供时间轴、分类、精选三种内容展示模式
|
||||
- **时间轴视图**: 按时间顺序展示专题相关内容发展脉络
|
||||
- **分类视图**: 按内容类别组织专题文章
|
||||
- **精选视图**: 展示专题中高质量内容
|
||||
- **相关推荐**: 推荐相关专题和扩展阅读
|
||||
- **互动功能**: 分享、订阅、意见反馈等操作
|
||||
- **评论系统**: 专题评论、讨论互动
|
||||
|
||||
### 8. 评论组件 (comments.uvue)
|
||||
- **多层级评论**: 支持评论和回复的树形结构显示
|
||||
- **评论互动**: 点赞、回复、举报、删除等完整功能
|
||||
- **排序筛选**: 支持按时间、热度、回复数等多种排序方式
|
||||
- **实时更新**: 评论数据实时同步,支持分页加载
|
||||
- **内容审核**: 评论状态管理和内容过滤机制
|
||||
- **用户权限**: 根据用户身份显示不同操作权限
|
||||
- **响应式设计**: 适配不同屏幕尺寸的评论界面
|
||||
|
||||
## 数据结构
|
||||
|
||||
### 核心数据类型 (基于 UTSJSONObject)
|
||||
|
||||
```typescript
|
||||
// 内容数据
|
||||
export type InfoContent = UTSJSONObject
|
||||
// 字段: id, title, content, summary, author, published_at, quality_score,
|
||||
// view_count, like_count, share_count, original_language, source_url, tags
|
||||
|
||||
// 翻译数据
|
||||
export type TranslationData = UTSJSONObject
|
||||
// 字段: id, content_id, language_id, title, content, summary,
|
||||
// human_verified, quality_score, created_at
|
||||
|
||||
// 分类数据
|
||||
export type CategoryData = UTSJSONObject
|
||||
// 字段: id, name_key, display_order, is_active, icon, color
|
||||
|
||||
// 专题数据
|
||||
export type TopicData = UTSJSONObject
|
||||
// 字段: id, title, description, topic_type, status, cover_image_url,
|
||||
// content_count, view_count, created_at, updated_at
|
||||
|
||||
// 专题内容关联
|
||||
export type TopicContentData = UTSJSONObject
|
||||
// 字段: id, topic_id, content_id, display_order, editor_note, created_at
|
||||
|
||||
// 评论数据
|
||||
export type CommentData = UTSJSONObject
|
||||
// 字段: id, target_type, target_id, parent_id, author_id, author_name,
|
||||
// author_avatar, content, status, like_count, reply_count, level,
|
||||
// thread_path, is_liked, is_author, created_at, updated_at
|
||||
|
||||
// 评论回复数据
|
||||
export type CommentReplyData = UTSJSONObject
|
||||
// 字段: id, comment_id, author_id, author_name, target_name, content,
|
||||
// like_count, is_liked, is_author, created_at
|
||||
|
||||
// 聊天消息
|
||||
export type ChatMessageData = UTSJSONObject
|
||||
// 字段: id, session_id, type, content, created_at, metadata
|
||||
|
||||
// 用户设置
|
||||
export type UserSettingsData = UTSJSONObject
|
||||
// 字段: user_id, interface_language, preferred_languages, preferred_categories,
|
||||
// auto_translate, notification_enabled, reading_mode, font_size, theme
|
||||
```
|
||||
|
||||
### 工具函数
|
||||
|
||||
```typescript
|
||||
// 内容数据获取
|
||||
getContentId(), getContentTitle(), getContentSummary(), getContentContent()
|
||||
getContentAuthor(), getContentPublishedAt(), getContentQualityScore()
|
||||
getContentViewCount(), getContentLikeCount(), getContentShareCount()
|
||||
getContentOriginalLanguage(), getContentSourceUrl(), getContentTags()
|
||||
|
||||
// 时间格式化
|
||||
formatDateTime(), formatRelativeTime()
|
||||
|
||||
// 质量评分
|
||||
getQualityScoreText(), getQualityScoreColor()
|
||||
|
||||
// 分类和语言
|
||||
getCategoryDisplayName(), getLanguageName()
|
||||
```
|
||||
|
||||
## 技术规范
|
||||
|
||||
### UTS Android 开发规范遵循
|
||||
|
||||
1. **变量声明**: 使用 `let` 和 `const`,避免 `var`
|
||||
2. **类型系统**: 使用 `type` 而非 `interface`,避免复杂类型嵌套
|
||||
3. **数据结构**: 全部基于 `UTSJSONObject`,避免 `safeGet` 等函数
|
||||
4. **循环遍历**: 使用 `for` 循环,避免 `forEach` 和 `map`
|
||||
5. **条件判断**: 使用 `!== null` 而非 `!` 操作符
|
||||
6. **空值处理**: 使用 `??` 而非 `||` 进行默认值设置
|
||||
7. **CSS限制**: 只支持 `display: flex`,不支持 `grid`、`gap`、`calc()` 等
|
||||
|
||||
### 数据交互规范
|
||||
|
||||
1. **Supabase集成**: 使用 `supadb` 组件进行数据交互
|
||||
2. **响应式状态**: 使用 `ref()` 和 `computed()` 管理状态
|
||||
3. **生命周期**: 合理使用 `onMounted()` 和 `onUnmounted()`
|
||||
4. **错误处理**: 统一的错误处理和用户反馈机制
|
||||
|
||||
### UI/UX 设计规范
|
||||
|
||||
1. **响应式设计**: 大屏(>768px)和小屏(<768px)自适应
|
||||
2. **色彩系统**: 统一的色彩规范和主题支持
|
||||
3. **交互反馈**: 加载状态、错误提示、成功反馈
|
||||
4. **无障碍支持**: 语义化标签和清晰的视觉层次
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 数据处理优化
|
||||
- 使用 UTSJSONObject 避免类型转换开销
|
||||
- 实现数据分页和虚拟滚动
|
||||
- 合理的缓存策略和数据预加载
|
||||
|
||||
### 渲染性能优化
|
||||
- 使用 `v-if` 而非 `v-show` 控制渲染
|
||||
- 避免深层嵌套和复杂计算属性
|
||||
- 合理使用 `key` 优化列表渲染
|
||||
|
||||
### 内存管理
|
||||
- 及时清理事件监听器和定时器
|
||||
- 避免内存泄漏和循环引用
|
||||
- 合理的组件生命周期管理
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 功能测试
|
||||
- 使用 `test-utils.uts` 进行单元测试
|
||||
- 验证数据结构和工具函数正确性
|
||||
- 模拟用户操作和边界条件测试
|
||||
|
||||
### 性能测试
|
||||
- 大数据量处理性能测试
|
||||
- 内存使用和渲染性能监控
|
||||
- 网络请求和响应时间优化
|
||||
|
||||
### 兼容性测试
|
||||
- 不同屏幕尺寸适配测试
|
||||
- Android 版本兼容性验证
|
||||
- 多语言显示和交互测试
|
||||
|
||||
## 部署和维护
|
||||
|
||||
### 开发环境
|
||||
- HBuilderX 3.8+
|
||||
- uni-app-x 框架
|
||||
- Supabase 数据库
|
||||
|
||||
### 构建和部署
|
||||
- Android APK 打包
|
||||
- 性能监控和错误报告
|
||||
- 版本更新和热修复机制
|
||||
|
||||
### 数据库维护
|
||||
- Supabase 表结构管理
|
||||
- 数据备份和恢复
|
||||
- 性能监控和优化
|
||||
|
||||
## 扩展计划
|
||||
|
||||
### 功能扩展
|
||||
- [ ] 离线阅读支持
|
||||
- [ ] 音频朗读功能
|
||||
- [ ] 内容评论系统
|
||||
- [ ] 社交分享集成
|
||||
- [ ] 个性化推荐算法优化
|
||||
|
||||
### 技术升级
|
||||
- [ ] PWA 支持
|
||||
- [ ] 深色模式完善
|
||||
- [ ] 无障碍功能增强
|
||||
- [ ] 性能监控仪表板
|
||||
|
||||
## 评论系统架构
|
||||
|
||||
### 评论数据模型
|
||||
|
||||
评论系统采用**树形结构**设计,支持多层级嵌套回复:
|
||||
|
||||
```sql
|
||||
-- 评论表 (ak_comments)
|
||||
CREATE TABLE ak_comments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
target_type VARCHAR(20) NOT NULL, -- 'content' | 'topic'
|
||||
target_id UUID NOT NULL, -- 目标内容/专题ID
|
||||
parent_id UUID, -- 父评论ID (NULL表示顶级评论)
|
||||
author_id UUID NOT NULL, -- 评论作者ID
|
||||
author_name VARCHAR(100) NOT NULL, -- 作者显示名称
|
||||
author_avatar TEXT, -- 作者头像URL
|
||||
content TEXT NOT NULL, -- 评论内容
|
||||
status VARCHAR(20) DEFAULT 'active', -- 评论状态
|
||||
like_count INTEGER DEFAULT 0, -- 点赞数
|
||||
reply_count INTEGER DEFAULT 0, -- 回复数
|
||||
level INTEGER DEFAULT 0, -- 评论层级 (0=顶级, 1=一级回复, ...)
|
||||
thread_path TEXT, -- 线程路径 (如: "1/5/12")
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### 评论功能特性
|
||||
|
||||
#### 1. **多层级结构**
|
||||
- **顶级评论**: 直接针对内容/专题的评论
|
||||
- **嵌套回复**: 支持对评论的回复,形成对话链
|
||||
- **层级限制**: 最多支持3层嵌套,避免过深结构
|
||||
- **线程追踪**: 通过thread_path字段追踪评论关系
|
||||
|
||||
#### 2. **交互功能**
|
||||
- **点赞系统**: 支持评论点赞/取消点赞
|
||||
- **回复机制**: @用户回复,保持对话连贯性
|
||||
- **删除权限**: 作者可删除自己的评论
|
||||
- **举报功能**: 支持6种举报类型的内容审核
|
||||
|
||||
#### 3. **内容管理**
|
||||
- **状态控制**: active/hidden/deleted/pending_review/rejected
|
||||
- **自动审核**: 关键词过滤和AI内容检测
|
||||
- **人工审核**: 举报内容的人工复审机制
|
||||
- **批量操作**: 管理员批量处理评论功能
|
||||
|
||||
#### 4. **排序和展示**
|
||||
- **时间排序**: 最新/最早评论优先
|
||||
- **热度排序**: 按点赞数排序
|
||||
- **回复排序**: 按回复数排序
|
||||
- **智能排序**: 综合质量和时间的算法排序
|
||||
|
||||
### 评论组件设计
|
||||
|
||||
#### **组件特点**
|
||||
- **高度复用**: 同时支持内容评论和专题评论
|
||||
- **响应式设计**: 适配不同屏幕尺寸
|
||||
- **性能优化**: 虚拟滚动和分页加载
|
||||
- **UTS规范**: 严格遵循UTS Android开发规范
|
||||
|
||||
#### **接口设计**
|
||||
```typescript
|
||||
interface CommentProps {
|
||||
targetType: 'content' | 'topic' // 评论目标类型
|
||||
targetId: string // 目标ID
|
||||
userId?: string // 当前用户ID
|
||||
userName?: string // 当前用户名
|
||||
}
|
||||
```
|
||||
|
||||
#### **状态管理**
|
||||
```typescript
|
||||
// 评论列表状态
|
||||
const commentsList = ref<Array<CommentData>>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const hasMore = ref<boolean>(true)
|
||||
|
||||
// 交互状态
|
||||
const activeReplyId = ref<string>('') // 当前回复的评论ID
|
||||
const showCommentInput = ref<boolean>(false) // 显示评论输入框
|
||||
const commentContent = ref<string>('') // 评论内容
|
||||
|
||||
// 排序和筛选
|
||||
const currentSort = ref<string>('created_at_desc')
|
||||
const showSortModal = ref<boolean>(false)
|
||||
```
|
||||
|
||||
### 评论业务流程
|
||||
|
||||
#### **发表评论流程**
|
||||
1. 用户点击"写评论"按钮
|
||||
2. 显示评论输入框,验证登录状态
|
||||
3. 输入评论内容,实时字数统计
|
||||
4. 提交评论,调用API进行内容检测
|
||||
5. 评论状态为pending_review或active
|
||||
6. 刷新评论列表,显示新评论
|
||||
|
||||
#### **回复评论流程**
|
||||
1. 点击评论的"回复"按钮
|
||||
2. 显示回复输入框,显示@目标用户
|
||||
3. 输入回复内容,支持@提及功能
|
||||
4. 提交回复,建立parent_id关系
|
||||
5. 更新父评论的reply_count
|
||||
6. 在评论树中正确显示回复
|
||||
|
||||
#### **评论审核流程**
|
||||
1. **自动审核**: 关键词过滤、长度检查
|
||||
2. **AI检测**: 违规内容、垃圾信息识别
|
||||
3. **人工审核**: 举报内容的人工复审
|
||||
4. **状态更新**: 根据审核结果更新评论状态
|
||||
5. **通知机制**: 审核结果通知评论作者
|
||||
|
||||
### 评论数据库优化
|
||||
|
||||
#### **索引策略**
|
||||
```sql
|
||||
-- 复合索引优化查询性能
|
||||
CREATE INDEX idx_comments_target ON ak_comments(target_type, target_id, status, created_at);
|
||||
CREATE INDEX idx_comments_parent ON ak_comments(parent_id, created_at);
|
||||
CREATE INDEX idx_comments_author ON ak_comments(author_id, created_at);
|
||||
CREATE INDEX idx_comments_thread ON ak_comments(thread_path);
|
||||
```
|
||||
|
||||
#### **查询优化**
|
||||
- **分页查询**: 使用cursor-based pagination
|
||||
- **预加载**: 评论作者信息和点赞状态预加载
|
||||
- **缓存策略**: 热门评论Redis缓存
|
||||
- **读写分离**: 评论读取和写入分离优化
|
||||
|
||||
### 评论系统集成
|
||||
|
||||
#### **与内容系统集成**
|
||||
- 内容详情页自动加载评论组件
|
||||
- 评论数统计实时更新到内容表
|
||||
- 评论质量影响内容推荐算法
|
||||
- 热门评论作为内容推荐依据
|
||||
|
||||
#### **与用户系统集成**
|
||||
- 用户评论历史和统计
|
||||
- 评论行为分析和画像构建
|
||||
- 评论权限和等级管理
|
||||
- 评论通知和消息推送
|
||||
|
||||
#### **与AI系统集成**
|
||||
- AI自动生成评论摘要
|
||||
- 智能评论推荐和排序
|
||||
- 评论情感分析和标签
|
||||
- AI辅助内容审核和过滤
|
||||
|
||||
## 联系和支持
|
||||
|
||||
如有问题或建议,请联系开发团队:
|
||||
- 技术支持: [支持邮箱]
|
||||
- 问题反馈: [反馈渠道]
|
||||
- 文档更新: [文档地址]
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2025年6月*
|
||||
*版本: v1.0.0*
|
||||
*开发团队: AI资讯系统开发组*
|
||||
1109
mall/pages/info/chat.uvue
Normal file
1109
mall/pages/info/chat.uvue
Normal file
File diff suppressed because it is too large
Load Diff
2860
mall/pages/info/comindex.uvue
Normal file
2860
mall/pages/info/comindex.uvue
Normal file
File diff suppressed because it is too large
Load Diff
370
mall/pages/info/cominfoedit.uvue
Normal file
370
mall/pages/info/cominfoedit.uvue
Normal file
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<view class="config-edit-page">
|
||||
<text class="page-title">公司全局配置管理</text>
|
||||
<button type="primary" @click="openAddModal">新增配置项</button>
|
||||
<list-view :loading="isLoading" class="config-list-view">
|
||||
<template #header>
|
||||
<view class="list-header-row">
|
||||
<text class="list-header-cell">配置键</text>
|
||||
<text class="list-header-cell">配置值</text>
|
||||
<text class="list-header-cell">类型</text>
|
||||
<text class="list-header-cell">分类</text>
|
||||
<text class="list-header-cell">可翻译</text>
|
||||
<text class="list-header-cell">排序</text>
|
||||
<text class="list-header-cell">启用</text>
|
||||
<text class="list-header-cell">操作</text>
|
||||
</view>
|
||||
</template>
|
||||
<list-item v-for="item in configList" :key="item.id" class="list-row">
|
||||
<text class="list-cell">{{ item.config_key }}</text>
|
||||
<text class="list-cell">{{ item.default_value }}</text>
|
||||
<text class="list-cell">{{ item.config_type }}</text>
|
||||
<text class="list-cell">{{ item.default_key }}</text>
|
||||
<text class="list-cell">{{ item.config_category }}</text>
|
||||
<text class="list-cell">{{ item.is_translatable ? '是' : '否' }}</text>
|
||||
<text class="list-cell">{{ item.sort_order }}</text>
|
||||
<view class="list-cell">
|
||||
<text
|
||||
:class="['tag', item.is_active ? 'tag-success' : 'tag-default']">{{ item.is_active ? '启用' : '停用' }}</text>
|
||||
</view>
|
||||
<view class="list-cell">
|
||||
<button size="mini" @click="openEditModal(item)">编辑</button>
|
||||
</view>
|
||||
</list-item>
|
||||
</list-view>
|
||||
<button type="default" @click="loadConfigList">刷新</button>
|
||||
|
||||
<!-- 编辑/新增弹窗 -->
|
||||
<l-popup v-model="editPopupVisible" position="bottom" :closeable="true" @click-close="closeEditModal">
|
||||
<view class="popup-content">
|
||||
<view class="popup-title-bar">
|
||||
<text class="popup-title">{{ editMode === 'add' ? '新增配置项' : '编辑配置项' }}</text>
|
||||
<button class="popup-close-btn" @click="closeEditModal">关闭</button>
|
||||
</view>
|
||||
<form @submit="saveConfig">
|
||||
<view class="form-item">
|
||||
<text class="form-label">配置键</text>
|
||||
<input v-model="editForm.config_key" placeholder="如 social_weibo_url" required />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">提示键</text>
|
||||
<input v-model="editForm.default_key" placeholder="如 social_weibo_url" required />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">配置值</text>
|
||||
<input v-model="editForm.default_value" placeholder="如 https://weibo.com/innovation-tech" required />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">类型</text>
|
||||
<input v-model="editForm.config_type" placeholder="如 string" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">分类</text>
|
||||
<input v-model="editForm.config_category" placeholder="如 social" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">可翻译</text>
|
||||
<switch :checked="!!editForm.is_translatable" @change="e => editForm.is_translatable = e.detail.value" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">排序</text>
|
||||
<input v-model="editForm.sort_order" type="number" placeholder="如 31" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">启用</text>
|
||||
<switch :checked="!!editForm.is_active" @change="e => editForm.is_active = e.detail.value" />
|
||||
</view>
|
||||
<view class="popup-actions">
|
||||
<button type="primary" form-type="submit">保存</button>
|
||||
<button @click="closeEditModal" type="button">取消</button>
|
||||
</view>
|
||||
</form>
|
||||
</view>
|
||||
</l-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type CompanyConfig = {
|
||||
id ?: string
|
||||
config_key : string
|
||||
default_key : string
|
||||
default_value : string
|
||||
config_type : string
|
||||
config_category ?: string
|
||||
is_translatable ?: boolean
|
||||
sort_order ?: number
|
||||
is_active ?: boolean
|
||||
created_at ?: string
|
||||
updated_at ?: string
|
||||
}
|
||||
|
||||
const configList = ref<Array<CompanyConfig>>([])
|
||||
const isLoading = ref(false)
|
||||
const editMode = ref<'add' | 'edit'>('add')
|
||||
const editForm = reactive<CompanyConfig>({
|
||||
config_key: '',
|
||||
default_key:'',
|
||||
default_value: '',
|
||||
config_type: '',
|
||||
config_category: '',
|
||||
is_translatable: true,
|
||||
sort_order: 0,
|
||||
is_active: true
|
||||
})
|
||||
const editPopupVisible = ref(false)
|
||||
|
||||
function loadConfigList() {
|
||||
isLoading.value = true
|
||||
supa
|
||||
.from('ak_global_config')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false })
|
||||
.execute()
|
||||
.then((res) => {
|
||||
configList.value = res.data ?? []
|
||||
})
|
||||
.catch((err) => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function openAddModal() {
|
||||
editMode.value = 'add'
|
||||
Object.assign(editForm, {
|
||||
config_key: '',
|
||||
default_value: '',
|
||||
config_type: '',
|
||||
config_category: '',
|
||||
is_translatable: true,
|
||||
sort_order: 0,
|
||||
is_active: true
|
||||
})
|
||||
editPopupVisible.value = true
|
||||
}
|
||||
|
||||
function openEditModal(item : CompanyConfig) {
|
||||
editMode.value = 'edit'
|
||||
Object.assign(editForm, item)
|
||||
editPopupVisible.value = true
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
editPopupVisible.value = false
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
if (!editForm.config_key || !editForm.default_value) {
|
||||
uni.showToast({ title: '请填写必填项', icon: 'none' })
|
||||
return
|
||||
}
|
||||
isLoading.value = true
|
||||
if (editMode.value === 'add') {
|
||||
supa
|
||||
.from('ak_global_config')
|
||||
.insert([editForm])
|
||||
.execute()
|
||||
.then(() => {
|
||||
uni.showToast({ title: '新增成功', icon: 'success' })
|
||||
loadConfigList()
|
||||
closeEditModal()
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '新增失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
} else {
|
||||
supa
|
||||
.from('ak_global_config')
|
||||
.update(editForm)
|
||||
.eq('id', editForm.id)
|
||||
.execute()
|
||||
.then(() => {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
loadConfigList()
|
||||
closeEditModal()
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function deleteConfig(id : string) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该配置项吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
isLoading.value = true
|
||||
supa
|
||||
.from('ak_global_config')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
.then(() => {
|
||||
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||
loadConfigList()
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfigList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.config-edit-page {
|
||||
padding: 20px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 18px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.config-list-view {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.list-header-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: #f1f5f9;
|
||||
padding: 8px 0;
|
||||
font-weight: bold;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.list-header-cell {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.list-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #e5e7eb;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.list-cell {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background: #9ca3af;
|
||||
margin: 0 auto;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tag-success {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.tag-default {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.popup-title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.popup-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #475569;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
width: 80px;
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
1000
mall/pages/info/comments.uvue
Normal file
1000
mall/pages/info/comments.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1106
mall/pages/info/detail.uvue
Normal file
1106
mall/pages/info/detail.uvue
Normal file
File diff suppressed because it is too large
Load Diff
934
mall/pages/info/index.uvue
Normal file
934
mall/pages/info/index.uvue
Normal file
@@ -0,0 +1,934 @@
|
||||
<!-- 多语言AI资讯系统主页 - 严格遵循UTS Android开发规范 -->
|
||||
<template>
|
||||
<scroll-view direction="vertical" class="info-home" :enable-back-to-top="true">
|
||||
<view class="header">
|
||||
<view class="header-content">
|
||||
<view class="header-actions">
|
||||
<view class="action-btn" @click="showLanguageSelector">
|
||||
<text class="action-text">{{ currentLanguageName }}{{$t('mt.category.politics')}}</text>
|
||||
</view>
|
||||
<view class="action-btn" @click="navigateToTopics">
|
||||
<text class="action-icon">📑</text>
|
||||
</view>
|
||||
<view class="action-btn" @click="navigateToSearch">
|
||||
<text class="action-icon">🔍</text>
|
||||
</view>
|
||||
<view class="action-btn" @click="navigateToChat">
|
||||
<text class="action-icon">💬</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类标签栏 -->
|
||||
<view class="category-section">
|
||||
<scroll-view direction="horizontal" class="category-scroll" >
|
||||
<view style="white-space: nowrap;flex-direction: row;">
|
||||
<view
|
||||
v-for="(category, index) in categoriesList"
|
||||
:key="category.id"
|
||||
class="category-tab"
|
||||
:class="{ 'is-active': selectedCategoryId === category.id, 'is-last': index === categoriesList.length - 1 }"
|
||||
@click="selectCategory(category)"
|
||||
style="display: inline-block; margin-right: 12px;">
|
||||
<text class="category-text" :class="{ 'is-active': selectedCategoryId === category.id }">{{ getCategoryName(category) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 精选内容区域 -->
|
||||
<view class="featured-section" v-if="featuredContentsList.length > 0">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ $t('mt.section.featured') }}</text>
|
||||
</view>
|
||||
<scroll-view direction="horizontal" class="featured-scroll" :scroll-x="true">
|
||||
<view class="featured-cards">
|
||||
<view
|
||||
v-for="(content, index) in featuredContentsList"
|
||||
:key="content.id"
|
||||
class="featured-card"
|
||||
:style="{ width: cardWidth + 'px' }"
|
||||
@click="navigateToDetail(content)">
|
||||
<view class="card-header">
|
||||
<text class="card-title">{{ content.title }}</text>
|
||||
<view class="quality-badge" :style="{ backgroundColor: getQualityScoreColorLocal(content.quality_score) }">
|
||||
<text class="quality-text">{{ getQualityScoreTextLocal(content.quality_score) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="card-summary">{{ content.summary }}</text>
|
||||
<view class="card-footer">
|
||||
<text class="card-author">{{ content.author }}</text>
|
||||
<text class="card-time">{{ formatRelativeTimeLocal(content.published_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 内容列表区域 -->
|
||||
<view class="content-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ $t('mt.section.latest') }}</text>
|
||||
<view class="section-actions">
|
||||
<view class="sort-btn" @click="showSortOptions">
|
||||
<text class="sort-text">{{ sortOptionText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-section" v-if="pageState.loading">
|
||||
<text class="loading-text">{{ $t('mt.status.loading') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<view class="error-section" v-if="pageState.error !== null">
|
||||
<text class="error-text">{{ pageState.error }}</text>
|
||||
<view class="retry-btn" @click="retryLoad">
|
||||
<text class="retry-text">{{ $t('mt.action.retry') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容列表 -->
|
||||
<view class="content-list" v-if="contentsList.length > 0">
|
||||
<view
|
||||
v-for="(content, index) in contentsList"
|
||||
:key="content.id"
|
||||
class="content-item"
|
||||
@click="navigateToDetail(content)">
|
||||
<view class="content-header">
|
||||
<text class="content-title">{{ content.title }}</text>
|
||||
<view class="content-meta">
|
||||
<!-- <text class="content-category">{{ getCategoryDisplayNameByIdLocal(content.category_id) }}</text> -->
|
||||
<text class="content-time">{{ formatRelativeTimeLocal(content.published_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="content-summary">{{ content.summary }}</text>
|
||||
<view class="content-footer">
|
||||
<view class="content-stats">
|
||||
<text class="stat-item">👁 {{ content.view_count }}</text>
|
||||
<text class="stat-item">👍 {{ content.like_count }}</text>
|
||||
<text class="stat-item">📤 {{ content.share_count }}</text>
|
||||
</view>
|
||||
<view class="quality-badge" :style="{ backgroundColor: getQualityScoreColorLocal(content.quality_score) }">
|
||||
<text class="quality-text">{{ getQualityScoreTextLocal(content.quality_score) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-section" v-if="contentsList.length === 0 && !pageState.loading && pageState.error === null">
|
||||
<text class="empty-text">{{ $t('mt.empty.content') }}</text>
|
||||
<view class="refresh-btn" @click="refreshData">
|
||||
<text class="refresh-text">{{ $t('mt.action.refresh') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more-section" v-if="contentsList.length > 0 && hasMore">
|
||||
<view class="load-more-btn" @click="loadMore" v-if="!loadingMore">
|
||||
<text class="load-more-text">{{ $t('mt.button.loadMore') }}</text>
|
||||
</view>
|
||||
<view class="loading-more" v-if="loadingMore">
|
||||
<text class="loading-more-text">{{ $t('mt.loadingMore') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 语言选择弹窗 -->
|
||||
<view class="modal-overlay" v-if="showLanguageModal" @click="hideLanguageSelector">
|
||||
<view class="language-modal" @click.stop="">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ $t('mt.modal.selectLanguage') }}</text>
|
||||
<view class="modal-close" @click="hideLanguageSelector">
|
||||
<text class="close-text">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="language-list">
|
||||
<view
|
||||
v-for="(language, index) in languagesList"
|
||||
:key="language.id"
|
||||
class="language-item"
|
||||
:class="{ active: currentLanguageCode === language.code }"
|
||||
@click="selectLanguage(language)">
|
||||
<text class="language-name">{{ getLanguageDisplayNameLocal(language.code) }}</text>
|
||||
<text class="language-native">{{ language.native_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 排序选择弹窗 -->
|
||||
<view class="modal-overlay" v-if="showSortModal" @click="hideSortOptions">
|
||||
<view class="sort-modal">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ $t('mt.modal.sort') }}</text>
|
||||
<view class="modal-close" @click="hideSortOptions">
|
||||
<text class="close-text">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="sort-list">
|
||||
<view
|
||||
v-for="(option, index) in sortOptionsList"
|
||||
:key="option.value"
|
||||
class="sort-item"
|
||||
:class="{ active: currentSortOption === option.value }"
|
||||
@click="selectSortOption(option)">
|
||||
<text class="sort-name">{{ option.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import {OrderOptions} from '@/components/supadb/aksupa.uts'
|
||||
|
||||
import {
|
||||
OptionItem,
|
||||
InfoContent,
|
||||
LanguageData,
|
||||
Language,
|
||||
PageState,
|
||||
ResponsiveState,
|
||||
|
||||
LANGUAGE_OPTIONS,
|
||||
SORT_OPTIONS,
|
||||
formatRelativeTimeKey,
|
||||
getQualityScoreColor,
|
||||
getQualityScoreText,
|
||||
CategoryData,
|
||||
CategoryTranslation
|
||||
} from './types.uts'
|
||||
|
||||
import { tt } from '@/utils/i18nfun.uts'
|
||||
|
||||
|
||||
|
||||
// 页面状态 - 严格使用简单类型避免复杂嵌套
|
||||
const pageState = ref<PageState>({
|
||||
loading: false,
|
||||
error: null,
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 响应式状态
|
||||
const responsiveState = ref<ResponsiveState>({
|
||||
isLargeScreen: false,
|
||||
isSmallScreen: true,
|
||||
screenWidth: 375,
|
||||
cardColumns: 1
|
||||
})
|
||||
|
||||
// UI状态变量 - 与template交互使用1维变量
|
||||
const showLanguageModal = ref<boolean>(false)
|
||||
const showSortModal = ref<boolean>(false)
|
||||
const loadingMore = ref<boolean>(false)
|
||||
const hasMore = ref<boolean>(true)
|
||||
|
||||
// 当前选择状态 - 使用简单字符串类型
|
||||
const selectedCategoryId = ref<string>('all')
|
||||
const currentLanguageCode = ref<string>('zh-CN')
|
||||
const currentLanguageName = ref<string>('简体中文')
|
||||
const currentSortOption = ref<string>('published_at_desc')
|
||||
const sortOptionText = ref<string>('最新发布')
|
||||
|
||||
// 数据列表 - 直接使用强类型 InfoContent 数组
|
||||
const contentsList = ref<Array<InfoContent>>([])
|
||||
const featuredContentsList = ref<Array<InfoContent>>([])
|
||||
const categoriesList = ref<CategoryData[]>([])
|
||||
const languagesList = ref<Array<LanguageData>>([])
|
||||
|
||||
// 选项列表
|
||||
const sortOptionsList = ref<Array<OptionItem>>(SORT_OPTIONS)
|
||||
|
||||
// 计算属性
|
||||
const cardWidth = computed((): number => {
|
||||
return responsiveState.value.isLargeScreen ? 300 : 280
|
||||
})
|
||||
|
||||
const contentFilter = computed((): string => {
|
||||
let filter = "status=eq.published"
|
||||
if (selectedCategoryId.value !== '') {
|
||||
filter += `&category_id=eq.${selectedCategoryId.value}`
|
||||
}
|
||||
// 根据排序选项构建order参数
|
||||
const sortParts = currentSortOption.value.split('_')
|
||||
const column = sortParts.slice(0, -1).join('_')
|
||||
const direction = sortParts[sortParts.length - 1] === 'desc' ? 'desc' : 'asc'
|
||||
filter += `&order=${column}.${direction}`
|
||||
return filter
|
||||
})
|
||||
|
||||
// Supabase组件引用 - 移除,使用直接调用方式
|
||||
// const contentsRef = ref<SupadbComponentPublicInstance | null>(null)
|
||||
|
||||
|
||||
// 响应式处理
|
||||
const handleResize = () => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
const screenWidth = systemInfo.screenWidth
|
||||
responsiveState.value.screenWidth = screenWidth
|
||||
responsiveState.value.isLargeScreen = screenWidth >= 768
|
||||
responsiveState.value.isSmallScreen = screenWidth < 768
|
||||
responsiveState.value.cardColumns = screenWidth >= 768 ? 2 : 1
|
||||
}
|
||||
|
||||
|
||||
// 错误处理
|
||||
const handleError = (error: any) => {
|
||||
pageState.value.loading = false
|
||||
pageState.value.error = '加载失败,请稍后重试'
|
||||
console.error('Contents loading error:', error)
|
||||
}
|
||||
|
||||
// 处理内容数据
|
||||
const handleContentsData = (data: Array<InfoContent>) => {
|
||||
pageState.value.loading = false
|
||||
pageState.value.error = null
|
||||
|
||||
if (pageState.value.currentPage === 1) {
|
||||
contentsList.value = []
|
||||
}
|
||||
|
||||
// 直接赋值强类型数组
|
||||
contentsList.value = contentsList.value.concat(data)
|
||||
|
||||
// 检查是否还有更多数据
|
||||
hasMore.value = data.length === pageState.value.pageSize
|
||||
}
|
||||
|
||||
// 本地工具函数 - 只保留必要的辅助函数,内容相关 getter 可全部移除,模板直接用 content.xxx
|
||||
// 保留分类/语言/格式化等工具
|
||||
const formatRelativeTimeLocal = (dateString: string): string => {
|
||||
const key = formatRelativeTimeKey(dateString)
|
||||
const now = new Date()
|
||||
const date = new Date(dateString)
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const seconds = Math.floor(diff / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
if (key === 'mt.time.daysAgo') return tt(key, { days })
|
||||
if (key === 'mt.time.hoursAgo') return tt(key, { hours })
|
||||
if (key === 'mt.time.minutesAgo') return tt(key, { minutes })
|
||||
return tt(key)
|
||||
}
|
||||
|
||||
const getLanguageDisplayNameLocal = (languageCode: string): string => {
|
||||
return tt(languageCode)
|
||||
}
|
||||
const getQualityScoreTextLocal = (score: number): string => {
|
||||
// console.log(score)
|
||||
// return score.toString()
|
||||
return getQualityScoreText(score*100)
|
||||
}
|
||||
const getQualityScoreColorLocal = (score: number): string => {
|
||||
return getQualityScoreColor(score*100)
|
||||
}
|
||||
|
||||
|
||||
// 语言选择
|
||||
const showLanguageSelector = () => {
|
||||
showLanguageModal.value = true
|
||||
}
|
||||
|
||||
const hideLanguageSelector = () => {
|
||||
showLanguageModal.value = false
|
||||
}
|
||||
|
||||
const selectLanguage = (language: LanguageData) => {
|
||||
currentLanguageCode.value = language.code
|
||||
currentLanguageName.value = getLanguageDisplayNameLocal(language.code)
|
||||
hideLanguageSelector()
|
||||
}
|
||||
|
||||
// 排序选择
|
||||
const showSortOptions = () => {
|
||||
showSortModal.value = true
|
||||
}
|
||||
|
||||
const hideSortOptions = () => {
|
||||
showSortModal.value = false
|
||||
}
|
||||
|
||||
|
||||
// 排序选项类型定义,保证类型安全
|
||||
|
||||
// 排序选项转换,保证 v-for 传递类型安全
|
||||
// const sortOptionsListTyped: Array<OptionItem> = SORT_OPTIONS.map(opt => ({
|
||||
// value: opt.value,
|
||||
// text: opt.text
|
||||
// }))
|
||||
|
||||
// 加载内容数据 - 使用 executeAs 替代 supadb 组件
|
||||
const loadContents = async () => {
|
||||
if (supa === null) return
|
||||
pageState.value.loading = true
|
||||
pageState.value.error = null
|
||||
try {
|
||||
let query = supa.from('ak_contents')
|
||||
.select('*,ak_contents_category_id_fkey(name_key)', {})
|
||||
.order('published_at', { ascending: false } as OrderOptions)
|
||||
// 应用分类筛选
|
||||
if (selectedCategoryId.value !== 'all') {
|
||||
query = query.eq('category_id', selectedCategoryId.value)
|
||||
}
|
||||
// 应用分页
|
||||
const start = (pageState.value.currentPage - 1) * pageState.value.pageSize
|
||||
const end = start + pageState.value.pageSize - 1
|
||||
query = query.range(start, end)
|
||||
const result = await query.executeAs<InfoContent>()
|
||||
if (result.data !== null && Array.isArray(result.data)) {
|
||||
handleContentsData(result.data as Array<InfoContent>)
|
||||
} else {
|
||||
throw new Error('Failed to load contents')
|
||||
}
|
||||
} catch (e) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
const selectSortOption = async (option: OptionItem) => {
|
||||
currentSortOption.value = option.value
|
||||
sortOptionText.value = option.text
|
||||
hideSortOptions()
|
||||
pageState.value.currentPage = 1
|
||||
await loadContents()
|
||||
}
|
||||
// 分类选择
|
||||
const selectCategory = async (category: CategoryData) => {
|
||||
selectedCategoryId.value = category.id
|
||||
pageState.value.currentPage = 1
|
||||
await loadContents()
|
||||
}
|
||||
|
||||
const getCategoryName = (category: CategoryData):string => {
|
||||
const translations = category.translations;
|
||||
const name = translations?.[0]?.name??'--';
|
||||
return name;
|
||||
}
|
||||
|
||||
// 加载精选内容 - 使用 executeAs 替代模拟数据
|
||||
const loadFeaturedContents = async () => {
|
||||
if (supa === null) return
|
||||
try {
|
||||
const result = await supa.from('ak_contents')
|
||||
.select('*', {})
|
||||
.eq('is_featured', true)
|
||||
.order('published_at', { ascending: false })
|
||||
.limit(5)
|
||||
.executeAs<Array<InfoContent>>()
|
||||
if (result.data !== null && Array.isArray(result.data)) {
|
||||
featuredContentsList.value = result.data as Array<InfoContent>
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Featured contents loading error:', e)
|
||||
featuredContentsList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = async () => {
|
||||
if (loadingMore.value || !hasMore.value) return
|
||||
loadingMore.value = true
|
||||
pageState.value.currentPage += 1
|
||||
setTimeout(() => {
|
||||
loadContents().then(() => {
|
||||
loadingMore.value = false
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = async () => {
|
||||
pageState.value.currentPage = 1
|
||||
await loadContents()
|
||||
}
|
||||
|
||||
// 重试加载
|
||||
const retryLoad = async () => {
|
||||
pageState.value.error = null
|
||||
await loadContents()
|
||||
}
|
||||
|
||||
// 导航函数
|
||||
const navigateToDetail = (content: InfoContent) => {
|
||||
const contentId = content.id
|
||||
uni.navigateTo({
|
||||
url: `/pages/info/detail?id=${contentId}`
|
||||
})
|
||||
}
|
||||
|
||||
const navigateToSearch = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/info/search'
|
||||
})
|
||||
}
|
||||
|
||||
const navigateToChat = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/info/chat'
|
||||
})
|
||||
}
|
||||
|
||||
const navigateToTopics = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/info/topics'
|
||||
})
|
||||
}
|
||||
|
||||
// 从数据库动态加载分类
|
||||
const loadCategories = async () => {
|
||||
if (supa === null) return
|
||||
try {
|
||||
const lang = currentLanguageCode.value
|
||||
console.log(lang)
|
||||
const result = await supa
|
||||
.from('ak_content_categories')
|
||||
.select('*,translations:ak_content_category_translations(name)', {})
|
||||
.eq('translations.language_code', lang)
|
||||
.order('sort_order', { ascending: true })
|
||||
.executeAs<CategoryData>()
|
||||
console.log(result)
|
||||
if (result.data !== null && Array.isArray(result.data)) {
|
||||
categoriesList.value = result.data as CategoryData[]
|
||||
} else {
|
||||
categoriesList.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载分类失败:', e)
|
||||
categoriesList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
const initializeData = async () => {
|
||||
// 动态加载分类列表
|
||||
await loadCategories()
|
||||
|
||||
// 初始化语言列表(强类型 LanguageData)
|
||||
languagesList.value = []
|
||||
for (let i: Int = 0; i < LANGUAGE_OPTIONS.length; i++) {
|
||||
const language: LanguageData = LANGUAGE_OPTIONS[i]
|
||||
languagesList.value.push(language)
|
||||
}
|
||||
|
||||
// await loadContents()
|
||||
// await loadFeaturedContents()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initializeData()
|
||||
handleResize()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* uts-android 兼容性重构:
|
||||
1. 拆分所有嵌套选择器为扁平 class(如 .category-tab.is-active、.category-text.is-active)。
|
||||
2. 所有“最后一个”元素的 margin-right/margin-bottom 用 .is-last 控制,移除伪类。
|
||||
3. 移除所有 gap、flex-wrap、嵌套选择器、伪类等不兼容写法。
|
||||
4. 在注释中补充重构说明,便于后续维护。
|
||||
*/
|
||||
.info-home {
|
||||
flex: 1;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
.header {
|
||||
background-color: #ffffff;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #e2e8f0;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.action-btn {
|
||||
padding: 8px 12px;
|
||||
margin-left: 8px;
|
||||
background-color: #f1f5f9;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.action-text {
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
}
|
||||
.action-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
.category-section {
|
||||
background-color: #ffffff;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #e2e8f0;
|
||||
}
|
||||
/* 横向滚动兼容 UTS Android */
|
||||
.category-scroll {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
min-width: max-content;
|
||||
}
|
||||
.category-tab {
|
||||
flex: 0 0 auto;
|
||||
padding: 8px 16px;
|
||||
margin-right: 12px;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 20px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.category-tab.is-active {
|
||||
background-color: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
.category-tab.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.category-text {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.category-text.is-active {
|
||||
color: #ffffff;
|
||||
}
|
||||
.featured-section {
|
||||
background-color: #ffffff;
|
||||
margin-top: 8px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
.section-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.sort-btn {
|
||||
padding: 6px 12px;
|
||||
background-color: #f1f5f9;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.sort-text {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
.featured-scroll {
|
||||
height: 180px;
|
||||
}
|
||||
.featured-cards {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
.featured-card {
|
||||
background-color: #f8fafc;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-right: 12px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
.featured-card.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-title {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
line-height: 22px;
|
||||
}
|
||||
.quality-badge {
|
||||
background-color: #10b981;
|
||||
border-radius: 12px;
|
||||
padding: 4px 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.quality-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.card-summary {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card-author {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.card-time {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.content-section {
|
||||
background-color: #ffffff;
|
||||
margin-top: 8px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
.loading-section {
|
||||
padding: 40px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.error-section {
|
||||
padding: 40px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
color: #ef4444;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.retry-btn {
|
||||
background-color: #3b82f6;
|
||||
border-radius: 20px;
|
||||
padding: 8px 24px;
|
||||
}
|
||||
.retry-text {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.content-list {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
.content-item {
|
||||
background-color: #f8fafc;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
.content-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.content-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
line-height: 22px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.content-meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.content-category {
|
||||
font-size: 12px;
|
||||
color: #3b82f6;
|
||||
background-color: #eff6ff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.content-time {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.content-summary {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.content-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.content-stats {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.stat-item {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-right: 16px;
|
||||
}
|
||||
.stat-item.is-last {
|
||||
margin-right: 0;
|
||||
}
|
||||
.empty-section {
|
||||
padding: 60px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.refresh-btn {
|
||||
background-color: #3b82f6;
|
||||
border-radius: 20px;
|
||||
padding: 10px 24px;
|
||||
}
|
||||
.refresh-text {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.load-more-section {
|
||||
padding: 20px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.load-more-btn {
|
||||
background-color: #f1f5f9;
|
||||
border-radius: 20px;
|
||||
padding: 10px 24px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
.load-more-text {
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
}
|
||||
.loading-more {
|
||||
padding: 10px 24px;
|
||||
}
|
||||
.loading-more-text {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.language-modal, .sort-modal {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16px;
|
||||
margin: 20px;
|
||||
max-height: 500px;
|
||||
min-width: 280px;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #e2e8f0;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 16px;
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
.close-text {
|
||||
font-size: 20px;
|
||||
color: #64748b;
|
||||
}
|
||||
.language-list, .sort-list {
|
||||
max-height: 400px;
|
||||
}
|
||||
.language-item, .sort-item {
|
||||
padding: 16px 20px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #f1f5f9;
|
||||
}
|
||||
.language-item.is-active, .sort-item.is-active {
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
.language-name {
|
||||
font-size: 16px;
|
||||
color: #1f2937;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.language-name.is-active {
|
||||
color: #3b82f6;
|
||||
}
|
||||
.language-native {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
}
|
||||
</style>
|
||||
1397
mall/pages/info/search.uvue
Normal file
1397
mall/pages/info/search.uvue
Normal file
File diff suppressed because it is too large
Load Diff
1126
mall/pages/info/settings.uvue
Normal file
1126
mall/pages/info/settings.uvue
Normal file
File diff suppressed because it is too large
Load Diff
258
mall/pages/info/test-utils.uts
Normal file
258
mall/pages/info/test-utils.uts
Normal file
@@ -0,0 +1,258 @@
|
||||
// 测试工具函数 - 验证多语言AI资讯系统各组件功能
|
||||
// 严格遵循 UTS Android 开发规范
|
||||
|
||||
import type {
|
||||
InfoContent,
|
||||
TranslationData,
|
||||
CategoryData,
|
||||
ChatMessageData,
|
||||
UserSettingsData,
|
||||
PageState
|
||||
} from './types.uts'
|
||||
|
||||
import {
|
||||
getContentId,
|
||||
getContentTitle,
|
||||
getContentSummary,
|
||||
getContentContent,
|
||||
getContentAuthor,
|
||||
getContentPublishedAt,
|
||||
getContentQualityScore,
|
||||
getContentViewCount,
|
||||
getContentLikeCount,
|
||||
getContentShareCount,
|
||||
getContentOriginalLanguage,
|
||||
getContentSourceUrl,
|
||||
getContentTags,
|
||||
formatDateTime,
|
||||
formatRelativeTime,
|
||||
getQualityScoreText,
|
||||
getQualityScoreColor,
|
||||
CATEGORIES,
|
||||
LANGUAGES,
|
||||
SORT_OPTIONS,
|
||||
DEFAULT_PAGE_SIZE
|
||||
} from './types.uts'
|
||||
|
||||
// 测试数据生成器
|
||||
export const createMockContent = (): InfoContent => {
|
||||
const content: InfoContent = {}
|
||||
content.set('id', `content_${Date.now()}`)
|
||||
content.set('title', 'AI技术发展趋势分析')
|
||||
content.set('content', '人工智能技术正在快速发展...')
|
||||
content.set('summary', '本文分析了AI技术的最新发展趋势')
|
||||
content.set('author', 'AI研究院')
|
||||
content.set('published_at', new Date().toISOString())
|
||||
content.set('quality_score', 0.85)
|
||||
content.set('view_count', 1250)
|
||||
content.set('like_count', 89)
|
||||
content.set('share_count', 23)
|
||||
content.set('original_language', 'zh-CN')
|
||||
content.set('source_url', 'https://example.com/ai-trends')
|
||||
content.set('tags', ['人工智能', '机器学习', '深度学习'])
|
||||
return content
|
||||
}
|
||||
|
||||
export const createMockTranslation = (): TranslationData => {
|
||||
const translation: TranslationData = {}
|
||||
translation.set('id', `trans_${Date.now()}`)
|
||||
translation.set('content_id', 'content_123')
|
||||
translation.set('language_id', 'en-US')
|
||||
translation.set('title', 'AI Technology Development Trends Analysis')
|
||||
translation.set('content', 'Artificial intelligence technology is rapidly developing...')
|
||||
translation.set('summary', 'This article analyzes the latest AI development trends')
|
||||
translation.set('human_verified', true)
|
||||
translation.set('quality_score', 0.82)
|
||||
translation.set('created_at', new Date().toISOString())
|
||||
return translation
|
||||
}
|
||||
|
||||
export const createMockCategory = (): CategoryData => {
|
||||
const category: CategoryData = {}
|
||||
category.set('id', `cat_${Date.now()}`)
|
||||
category.set('name_key', 'technology')
|
||||
category.set('display_order', 1)
|
||||
category.set('is_active', true)
|
||||
category.set('icon', '')
|
||||
category.set('color', '#3b82f6')
|
||||
return category
|
||||
}
|
||||
|
||||
export const createMockChatMessage = (type: string): ChatMessageData => {
|
||||
const message: ChatMessageData = {}
|
||||
message.set('id', `msg_${Date.now()}`)
|
||||
message.set('session_id', 'session_123')
|
||||
message.set('type', type) // 'user' | 'assistant'
|
||||
message.set('content', type === 'user' ? '请推荐一些AI相关的资讯' : '为您推荐以下AI相关资讯...')
|
||||
message.set('created_at', new Date().toISOString())
|
||||
message.set('metadata', {})
|
||||
return message
|
||||
}
|
||||
|
||||
export const createMockUserSettings = (): UserSettingsData => {
|
||||
const settings: UserSettingsData = {}
|
||||
settings.set('user_id', 'user_123')
|
||||
settings.set('interface_language', 'zh-CN')
|
||||
settings.set('preferred_languages', ['zh-CN', 'en-US'])
|
||||
settings.set('preferred_categories', ['technology', 'science'])
|
||||
settings.set('auto_translate', true)
|
||||
settings.set('notification_enabled', true)
|
||||
settings.set('reading_mode', 'normal')
|
||||
settings.set('font_size', 'medium')
|
||||
settings.set('theme', 'light')
|
||||
return settings
|
||||
}
|
||||
|
||||
// 验证函数
|
||||
export const validateContentData = (content: InfoContent): boolean => {
|
||||
try {
|
||||
// 验证必需字段
|
||||
const id = getContentId(content)
|
||||
const title = getContentTitle(content)
|
||||
const contentText = getContentContent(content)
|
||||
const author = getContentAuthor(content)
|
||||
const publishedAt = getContentPublishedAt(content)
|
||||
|
||||
if (id === '' || title === '' || contentText === '' || author === '' || publishedAt === '') {
|
||||
console.error('Content validation failed: missing required fields')
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证数值字段
|
||||
const qualityScore = getContentQualityScore(content)
|
||||
const viewCount = getContentViewCount(content)
|
||||
const likeCount = getContentLikeCount(content)
|
||||
const shareCount = getContentShareCount(content)
|
||||
|
||||
if (qualityScore < 0 || qualityScore > 1) {
|
||||
console.error('Content validation failed: invalid quality_score')
|
||||
return false
|
||||
}
|
||||
|
||||
if (viewCount < 0 || likeCount < 0 || shareCount < 0) {
|
||||
console.error('Content validation failed: invalid count values')
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证标签数组
|
||||
const tags = getContentTags(content)
|
||||
if (tags.length > 10) {
|
||||
console.error('Content validation failed: too many tags')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Content validation error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const validateTranslationData = (translation: TranslationData): boolean => {
|
||||
try {
|
||||
const id = translation.get('id') as string
|
||||
const contentId = translation.get('content_id') as string
|
||||
const languageId = translation.get('language_id') as string
|
||||
const title = translation.get('title') as string
|
||||
const content = translation.get('content') as string
|
||||
|
||||
if (id === '' || contentId === '' || languageId === '' || title === '' || content === '') {
|
||||
console.error('Translation validation failed: missing required fields')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Translation validation error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 性能测试函数
|
||||
export const performanceTest = () => {
|
||||
console.log('开始性能测试...')
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
// 创建大量测试数据
|
||||
const contents: Array<InfoContent> = []
|
||||
for (let i: Int = 0; i < 1000; i++) {
|
||||
contents.push(createMockContent())
|
||||
}
|
||||
|
||||
// 测试数据处理速度
|
||||
let validCount: Int = 0
|
||||
for (let i: Int = 0; i < contents.length; i++) {
|
||||
const content = contents[i]
|
||||
if (validateContentData(content)) {
|
||||
validCount++
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = Date.now()
|
||||
const duration = endTime - startTime
|
||||
|
||||
console.log(`性能测试完成:`)
|
||||
console.log(`- 处理数据量: ${contents.length}`)
|
||||
console.log(`- 有效数据: ${validCount}`)
|
||||
console.log(`- 耗时: ${duration}ms`)
|
||||
console.log(`- 平均处理速度: ${contents.length / duration * 1000}条/秒`)
|
||||
}
|
||||
|
||||
// 功能测试函数
|
||||
export const functionalTest = () => {
|
||||
console.log('开始功能测试...')
|
||||
|
||||
// 测试内容数据处理
|
||||
const mockContent = createMockContent()
|
||||
console.log('✓ 内容数据创建成功')
|
||||
|
||||
const isValidContent = validateContentData(mockContent)
|
||||
console.log(`✓ 内容数据验证: ${isValidContent ? '通过' : '失败'}`)
|
||||
|
||||
// 测试翻译数据处理
|
||||
const mockTranslation = createMockTranslation()
|
||||
console.log('✓ 翻译数据创建成功')
|
||||
|
||||
const isValidTranslation = validateTranslationData(mockTranslation)
|
||||
console.log(`✓ 翻译数据验证: ${isValidTranslation ? '通过' : '失败'}`)
|
||||
|
||||
// 测试分类数据处理
|
||||
const mockCategory = createMockCategory()
|
||||
console.log('✓ 分类数据创建成功')
|
||||
|
||||
// 测试聊天消息处理
|
||||
const mockUserMessage = createMockChatMessage('user')
|
||||
const mockAssistantMessage = createMockChatMessage('assistant')
|
||||
console.log('✓ 聊天消息创建成功')
|
||||
|
||||
// 测试用户设置处理
|
||||
const mockSettings = createMockUserSettings()
|
||||
console.log('✓ 用户设置创建成功')
|
||||
|
||||
// 测试工具函数
|
||||
const testDateTime = new Date().toISOString()
|
||||
const formattedDateTime = formatDateTime(testDateTime)
|
||||
const relativeTime = formatRelativeTime(testDateTime)
|
||||
console.log(`✓ 时间格式化测试: ${formattedDateTime}, ${relativeTime}`)
|
||||
|
||||
const qualityScore = 0.85
|
||||
const qualityText = getQualityScoreText(qualityScore)
|
||||
const qualityColor = getQualityScoreColor(qualityScore)
|
||||
console.log(`✓ 质量分数测试: ${qualityText}, ${qualityColor}`)
|
||||
|
||||
console.log('功能测试完成!')
|
||||
}
|
||||
|
||||
// 集成测试函数
|
||||
export const runAllTests = () => {
|
||||
console.log('=== 多语言AI资讯系统测试开始 ===')
|
||||
|
||||
try {
|
||||
functionalTest()
|
||||
performanceTest()
|
||||
console.log('=== 所有测试通过 ===')
|
||||
} catch (error) {
|
||||
console.error('=== 测试失败 ===', error)
|
||||
}
|
||||
}
|
||||
37
mall/pages/info/test/i18n-test.uvue
Normal file
37
mall/pages/info/test/i18n-test.uvue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<view class="i18n-test-page">
|
||||
<view class="section">
|
||||
<text class="section-title">{{ $t('mt.title.news') }}</text>
|
||||
<text class="section-desc">{{ $t('prev') }}</text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
|
||||
import { tt } from '@/utils/i18nfun.uts'
|
||||
// 也可在脚本中直接调用 tSmart,参数智能判断
|
||||
const techCategory = tt('prev')
|
||||
console.log(techCategory)
|
||||
// const enLang = tSmart('prev', 'en-US')
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.i18n-test-page {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
915
mall/pages/info/topic-detail.uvue
Normal file
915
mall/pages/info/topic-detail.uvue
Normal file
@@ -0,0 +1,915 @@
|
||||
<!-- 专题详情页面 - 专题内容展示和管理 -->
|
||||
<template>
|
||||
<scroll-view direction="vertical" class="topic-detail" :scroll-y="true" :enable-back-to-top="true">
|
||||
<!-- 专题头部 -->
|
||||
<view class="topic-header" v-if="topicData !== null">
|
||||
<view class="header-cover" :style="{ backgroundImage: `url(${topicData.cover_image})` }">
|
||||
<view class="header-overlay">
|
||||
<view class="back-btn" @click="goBack">
|
||||
<text class="back-icon">←</text>
|
||||
</view>
|
||||
<view class="topic-badges">
|
||||
<view class="type-badge" :style="{ backgroundColor: getTopicStatusColor(topicData.status) }">
|
||||
<text class="badge-text">{{ $t('mt.topic.type.' + topicData.topic_type) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-info">
|
||||
<text class="topic-title">{{ topicData.title }}</text>
|
||||
<text class="topic-description">{{ topicData.description }}</text>
|
||||
<view class="topic-meta">
|
||||
<view class="meta-stats">
|
||||
<text class="stat-item">📄 {{ topicData.content_count }}{{ $t('mt.topic.contentCountUnit') }}</text>
|
||||
<text class="stat-item">👁 {{ topicData.view_count }}{{ $t('mt.topic.viewCountUnit') }}</text>
|
||||
<text class="stat-item">📅 {{ formatRelativeTimeKey(topicData.updated_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 专题内容区域 -->
|
||||
<view class="topic-content">
|
||||
<!-- 内容筛选 -->
|
||||
<view class="content-filters">
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
class="filter-tab"
|
||||
:class="{ active: currentViewMode === 'timeline' }"
|
||||
@click="setViewMode('timeline')">
|
||||
<text class="filter-text">{{ $t('mt.topic.filter.timeline') }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="filter-tab"
|
||||
:class="{ active: currentViewMode === 'category' }"
|
||||
@click="setViewMode('category')">
|
||||
<text class="filter-text">{{ $t('mt.topic.filter.category') }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="filter-tab"
|
||||
:class="{ active: currentViewMode === 'quality' }"
|
||||
@click="setViewMode('quality')">
|
||||
<text class="filter-text">{{ $t('mt.topic.filter.quality') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-section" v-if="contentState.loading">
|
||||
<text class="loading-text">{{ $t('loading') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<view class="error-section" v-if="contentState.error !== null">
|
||||
<text class="error-text">{{ contentState.error }}</text>
|
||||
<view class="retry-btn" @click="retryLoadContent">
|
||||
<text class="retry-text">{{ $t('mt.common.retry') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 时间轴视图 -->
|
||||
<view class="timeline-view" v-if="currentViewMode === 'timeline' && topicContentsList.length > 0">
|
||||
<view
|
||||
v-for="(content, index) in topicContentsList"
|
||||
:key="content.id"
|
||||
class="timeline-item">
|
||||
<view class="timeline-dot"></view>
|
||||
<view class="timeline-content" @click="navigateToContent(content)">
|
||||
<view class="timeline-header">
|
||||
<text class="timeline-title">{{ content.title }}</text>
|
||||
<text class="timeline-time">{{ formatRelativeTimeKey(content.published_at) }}</text>
|
||||
</view>
|
||||
<text class="timeline-summary">{{ content.summary }}</text>
|
||||
<view class="timeline-meta">
|
||||
<text class="meta-author">{{ content.author }}</text>
|
||||
<view class="quality-badge" :style="{ backgroundColor: getQualityScoreColor(content.quality_score) }">
|
||||
<text class="quality-text">{{ getQualityScoreText(content.quality_score) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类视图 -->
|
||||
<view class="category-view" v-if="currentViewMode === 'category' && topicContentsList.length > 0">
|
||||
<view
|
||||
v-for="(content, index) in topicContentsList"
|
||||
:key="content.id"
|
||||
class="category-item"
|
||||
@click="navigateToContent(content)">
|
||||
<view class="category-header">
|
||||
<text class="category-title">{{ content.title }}</text>
|
||||
<view class="category-badge">
|
||||
<text class="category-text">{{ content.category_id }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="category-summary">{{ content.summary }}</text>
|
||||
<view class="category-footer">
|
||||
<view class="category-stats">
|
||||
<text class="stat-text">👁 {{ content.view_count }}</text>
|
||||
<text class="stat-text">👍 {{ content.like_count }}</text>
|
||||
</view>
|
||||
<text class="category-time">{{ formatRelativeTimeKey(content.published_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 精选视图 -->
|
||||
<view class="quality-view" v-if="currentViewMode === 'quality' && topicContentsList.length > 0">
|
||||
<view
|
||||
v-for="(content, index) in qualityContentsList"
|
||||
:key="content.id"
|
||||
class="quality-item"
|
||||
@click="navigateToContent(content)">
|
||||
<view class="quality-header">
|
||||
<text class="quality-title">{{ content.title }}</text>
|
||||
<view class="quality-score" :style="{ backgroundColor: getQualityScoreColor(content.quality_score) }">
|
||||
<text class="score-text">{{ getQualityScoreText(content.quality_score) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="quality-summary">{{ content.summary }}</text>
|
||||
<view class="quality-footer">
|
||||
<text class="quality-author">{{ content.author }}</text>
|
||||
<view class="quality-stats">
|
||||
<text class="stat-text">👁 {{ content.view_count }}</text>
|
||||
<text class="stat-text">👍 {{ content.like_count }}</text>
|
||||
<text class="stat-text">📤 {{ content.share_count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-section" v-if="topicContentsList.length === 0 && !contentState.loading && contentState.error === null">
|
||||
<text class="empty-text">{{ $t('mt.common.empty') }}</text>
|
||||
<view class="refresh-btn" @click="refreshContent">
|
||||
<text class="refresh-text">{{ $t('mt.common.refresh') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more-section" v-if="topicContentsList.length > 0 && hasMoreContent">
|
||||
<view class="load-more-btn" @click="loadMoreContent" v-if="!loadingMoreContent">
|
||||
<text class="load-more-text">{{ $t('mt.common.loadMore') }}</text>
|
||||
</view>
|
||||
<view class="loading-more" v-if="loadingMoreContent">
|
||||
<text class="loading-more-text">{{ $t('mt.common.loadingMore') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 相关专题推荐 -->
|
||||
<view class="related-topics" v-if="relatedTopicsList.length > 0">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ $t('mt.topic.related') }}</text>
|
||||
</view>
|
||||
<scroll-view direction="horizontal" class="related-scroll" :scroll-x="true">
|
||||
<view class="related-list">
|
||||
<view
|
||||
v-for="(topic, index) in relatedTopicsList"
|
||||
:key="topic.id"
|
||||
class="related-item"
|
||||
@click="navigateToTopic(topic)">
|
||||
<text class="related-title">{{ topic.title }}</text>
|
||||
<text class="related-desc">{{ topic.description }}</text>
|
||||
<view class="related-stats">
|
||||
<text class="related-stat">{{ topic.content_count }}{{ $t('mt.topic.contentCountUnit') }}</text>
|
||||
<text class="related-stat">{{ topic.view_count }}{{ $t('mt.topic.viewCountUnit') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 评论区域 -->
|
||||
<view class="comments-section" v-if="topicData !== null">
|
||||
<Comments
|
||||
:targetType="'topic'"
|
||||
:targetId="topicData.id"
|
||||
:userId="currentUserId"
|
||||
:userName="currentUserName">
|
||||
</Comments>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom-actions">
|
||||
<view class="action-item" @click="shareTopic">
|
||||
<text class="action-icon">📤</text>
|
||||
<text class="action-text">{{ $t('mt.topic.action.share') }}</text>
|
||||
</view>
|
||||
<view class="action-item" @click="subscribeTopic">
|
||||
<text class="action-icon">🔔</text>
|
||||
<text class="action-text">{{ $t('mt.topic.action.subscribe') }}</text>
|
||||
</view>
|
||||
<view class="action-item" @click="openChat">
|
||||
<text class="action-icon">💬</text>
|
||||
<text class="action-text">{{ $t('mt.topic.action.aiAssistant') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Comments from './comments.uvue'
|
||||
import {
|
||||
Topic,
|
||||
InfoContent,
|
||||
PageState,
|
||||
formatRelativeTimeKey,
|
||||
getTopicStatusColor,
|
||||
getQualityScoreText,
|
||||
getQualityScoreColor
|
||||
} from './types.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { tt } from '@/utils/i18nfun.uts'
|
||||
import i18n from '@/uni_modules/i18n/index.uts' // 保留用于语言切换
|
||||
|
||||
// 页面参数
|
||||
const topicId = ref<string>('')
|
||||
|
||||
// 专题数据
|
||||
const topicData = ref<Topic | null>(null)
|
||||
|
||||
// 内容状态
|
||||
const contentState = ref<PageState>({
|
||||
loading: false,
|
||||
error: null,
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// UI状态
|
||||
const currentViewMode = ref<string>('timeline')
|
||||
const hasMoreContent = ref<boolean>(true)
|
||||
const loadingMoreContent = ref<boolean>(false)
|
||||
const contentPageSize = ref<number>(20)
|
||||
|
||||
// 用户信息 - 评论功能需要
|
||||
const currentUserId = ref<string>('user-demo-001')
|
||||
const currentUserName = ref<string>('演示用户')
|
||||
|
||||
// 数据列表
|
||||
const topicContentsList = ref<Array<InfoContent>>([])
|
||||
const relatedTopicsList = ref<Array<Topic>>([])
|
||||
|
||||
// 计算属性
|
||||
const qualityContentsList = computed((): Array<InfoContent> => {
|
||||
return topicContentsList.value.filter(c => c.quality_score >= 0.8)
|
||||
})
|
||||
|
||||
const topicContentFilter = computed((): string => {
|
||||
let filter = `topic_id=eq.${topicId.value}`
|
||||
|
||||
// 根据视图模式调整排序
|
||||
if (currentViewMode.value === 'timeline') {
|
||||
filter += "&order=display_order.asc"
|
||||
} else if (currentViewMode.value === 'category') {
|
||||
filter += "&order=category_id.asc,display_order.asc"
|
||||
} else if (currentViewMode.value === 'quality') {
|
||||
filter += "&order=quality_score.desc,display_order.asc"
|
||||
}
|
||||
|
||||
return filter
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onLoad((options: OnLoadOptions) => {
|
||||
if (options.id !== undefined) {
|
||||
topicId.value = options.id as string
|
||||
}
|
||||
|
||||
if (topicId.value !== '') {
|
||||
loadTopicData()
|
||||
loadTopicContents()
|
||||
loadRelatedTopics()
|
||||
}
|
||||
})
|
||||
|
||||
// 加载专题数据
|
||||
const loadTopicData = async () => {
|
||||
try {
|
||||
const result = await supa.from('ak_topics')
|
||||
.select('*')
|
||||
.eq('id', topicId.value)
|
||||
.single()
|
||||
.executeAs<Topic>()
|
||||
if (result.error !== null) {
|
||||
console.error('Topic data loading error:', result.error)
|
||||
return
|
||||
}
|
||||
const data = result.data
|
||||
if (data !== null) {
|
||||
topicData.value = data
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Topic data loading error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载专题内容
|
||||
const loadTopicContents = async () => {
|
||||
contentState.value.loading = true
|
||||
contentState.value.error = null
|
||||
try {
|
||||
// TODO: 替换为实际接口调用
|
||||
// 这里模拟异步加载
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
// 假设返回模拟数据
|
||||
const mockContents: Array<InfoContent> = [
|
||||
{
|
||||
id: 'content-1',
|
||||
title: 'AI赋能医疗行业',
|
||||
summary: '人工智能正在深刻改变医疗行业的诊断和服务模式。',
|
||||
content: '',
|
||||
author: '张三',
|
||||
published_at: '2025-07-01T10:00:00Z',
|
||||
quality_score: 92,
|
||||
view_count: 1200,
|
||||
like_count: 88,
|
||||
share_count: 12,
|
||||
category_id: 'health',
|
||||
original_language: 'zh-CN',
|
||||
source_url: '',
|
||||
tags: ['AI', '医疗'],
|
||||
created_at: '2025-07-01T09:00:00Z',
|
||||
updated_at: '2025-07-01T10:00:00Z'
|
||||
}
|
||||
]
|
||||
topicContentsList.value = mockContents
|
||||
// 可根据分页逻辑设置 hasMoreContent
|
||||
hasMoreContent.value = false
|
||||
} catch (e) {
|
||||
contentState.value.error = '加载失败,请稍后重试'
|
||||
} finally {
|
||||
contentState.value.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理专题内容数据
|
||||
const handleTopicContentsData = (data: any) => {
|
||||
contentState.value.loading = false
|
||||
contentState.value.error = null
|
||||
if (data !== null && Array.isArray(data)) {
|
||||
const newContents = data as Array<InfoContent>
|
||||
if (contentState.value.currentPage === 1) {
|
||||
topicContentsList.value = newContents
|
||||
} else {
|
||||
topicContentsList.value = topicContentsList.value.concat(newContents)
|
||||
}
|
||||
hasMoreContent.value = newContents.length === contentPageSize.value
|
||||
}
|
||||
}
|
||||
|
||||
// 加载相关专题
|
||||
const loadRelatedTopics = () => {
|
||||
// 模拟相关专题数据
|
||||
const relatedTopic: Topic = {
|
||||
id: 'topic-related-1',
|
||||
title: '机器学习算法详解',
|
||||
description: '深入解析机器学习核心算法原理和应用',
|
||||
created_by: '',
|
||||
is_active: true,
|
||||
content_count: 12,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
view_count: 15600
|
||||
}
|
||||
relatedTopicsList.value = [relatedTopic]
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
const handleContentError = (error: any) => {
|
||||
contentState.value.loading = false
|
||||
contentState.value.error = '加载失败,请稍后重试'
|
||||
console.error('Topic contents loading error:', error)
|
||||
}
|
||||
|
||||
// 视图模式切换
|
||||
const setViewMode = (mode: string) => {
|
||||
currentViewMode.value = mode
|
||||
contentState.value.currentPage = 1
|
||||
loadTopicContents()
|
||||
}
|
||||
|
||||
// 加载更多内容
|
||||
const loadMoreContent = () => {
|
||||
if (loadingMoreContent.value || !hasMoreContent.value) return
|
||||
|
||||
loadingMoreContent.value = true
|
||||
contentState.value.currentPage += 1
|
||||
|
||||
setTimeout(() => {
|
||||
loadTopicContents()
|
||||
loadingMoreContent.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 刷新内容
|
||||
const refreshContent = () => {
|
||||
contentState.value.currentPage = 1
|
||||
loadTopicContents()
|
||||
}
|
||||
|
||||
// 重试加载内容
|
||||
const retryLoadContent = () => {
|
||||
contentState.value.error = null
|
||||
loadTopicContents()
|
||||
}
|
||||
|
||||
// 导航函数
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
const navigateToContent = (content: InfoContent) => {
|
||||
const contentId = content.id
|
||||
uni.navigateTo({
|
||||
url: `/pages/info/detail?id=${contentId}&from=topic`
|
||||
})
|
||||
}
|
||||
|
||||
const navigateToTopic = (topic: Topic) => {
|
||||
const relatedTopicId = topic.id
|
||||
uni.navigateTo({
|
||||
url: `/pages/info/topic-detail?id=${relatedTopicId}`
|
||||
})
|
||||
}
|
||||
|
||||
// 操作函数
|
||||
const shareTopic = () => {
|
||||
// 分享专题
|
||||
uni.showToast({
|
||||
title: '分享功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const subscribeTopic = () => {
|
||||
// 订阅专题
|
||||
uni.showToast({
|
||||
title: '订阅功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const openChat = () => {
|
||||
// 打开AI助手
|
||||
uni.navigateTo({
|
||||
url: `/pages/info/chat?context=topic&id=${topicId.value}`
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.topic-detail {
|
||||
flex: 1;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.topic-header {
|
||||
background-color: #ffffff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header-cover {
|
||||
height: 200px;
|
||||
background-color: #8b5cf6;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 16px;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 18px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.topic-badges {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
background-color: #8b5cf6;
|
||||
border-radius: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.topic-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
line-height: 32px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.topic-description {
|
||||
font-size: 16px;
|
||||
color: #64748b;
|
||||
line-height: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.topic-meta {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-stats {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.topic-content {
|
||||
background-color: #ffffff;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.content-filters {
|
||||
padding: 0 16px 16px 16px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
flex-direction: row;
|
||||
background-color: #f1f5f9;
|
||||
border-radius: 24px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
align-items: center;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
background-color: #8b5cf6;
|
||||
}
|
||||
|
||||
.filter-text {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.filter-tab.active .filter-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.timeline-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
flex-direction: row;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background-color: #8b5cf6;
|
||||
margin-top: 6px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.timeline-header {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
line-height: 22px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.timeline-summary {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.timeline-meta {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta-author {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.quality-badge {
|
||||
background-color: #10b981;
|
||||
border-radius: 10px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.quality-text {
|
||||
font-size: 11px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.category-view, .quality-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.category-item, .quality-item {
|
||||
background-color: #f8fafc;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.category-header, .quality-header {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.category-title, .quality-title {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
line-height: 22px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
background-color: #3b82f6;
|
||||
border-radius: 10px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.category-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.quality-score {
|
||||
background-color: #10b981;
|
||||
border-radius: 10px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.category-summary, .quality-summary {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.category-footer, .quality-footer {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.category-stats, .quality-stats {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-text {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.category-time, .quality-author {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading-section, .error-section {
|
||||
padding: 40px 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
color: #ef4444;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.retry-btn, .refresh-btn {
|
||||
background-color: #8b5cf6;
|
||||
border-radius: 20px;
|
||||
padding: 8px 24px;
|
||||
}
|
||||
|
||||
.retry-text, .refresh-text {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.empty-section {
|
||||
padding: 60px 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.load-more-section {
|
||||
padding: 20px 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.load-more-btn {
|
||||
background-color: #f1f5f9;
|
||||
border-radius: 20px;
|
||||
padding: 10px 24px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.load-more-text {
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
padding: 10px 24px;
|
||||
}
|
||||
|
||||
.loading-more-text {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.related-topics {
|
||||
background-color: #ffffff;
|
||||
margin-top: 8px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 0 16px 12px 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.related-scroll {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.related-list {
|
||||
flex-direction: row;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.related-item {
|
||||
width: 200px;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
margin-right: 12px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.related-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
line-height: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.related-desc {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.related-stats {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.related-stat {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.comments-section {
|
||||
background-color: #ffffff;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
background-color: #ffffff;
|
||||
flex-direction: row;
|
||||
padding: 16px;
|
||||
border-top-width: 1px;
|
||||
border-top-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
</style>
|
||||
829
mall/pages/info/topics.uvue
Normal file
829
mall/pages/info/topics.uvue
Normal file
@@ -0,0 +1,829 @@
|
||||
<!-- 专题页面 - 专题列表和专题详情 -->
|
||||
<template>
|
||||
<scroll-view direction="vertical" class="topics-page" :scroll-y="true" :enable-back-to-top="true">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="header">
|
||||
<view class="header-content">
|
||||
<text class="title">{{ $t('mt.topic.hot') }}</text>
|
||||
<view class="header-actions">
|
||||
<view class="action-btn" @click="showTypeSelector">
|
||||
<text class="action-text">{{ currentTypeText }}</text>
|
||||
</view>
|
||||
<view class="action-btn" @click="navigateToSearch">
|
||||
<text class="action-icon">🔍</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 专题类型筛选 -->
|
||||
<view class="type-section">
|
||||
<scroll-view direction="horizontal" class="type-scroll" :scroll-x="true">
|
||||
<view class="type-tabs">
|
||||
<view
|
||||
v-for="(type, index) in topicTypesList"
|
||||
:key="type.value"
|
||||
class="type-tab"
|
||||
:class="{ active: selectedTypeValue === type.value }"
|
||||
@click="selectTopicType(type)">
|
||||
<text class="type-text">{{ type.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 精选专题 -->
|
||||
<view class="featured-section" v-if="featuredTopicsList.length > 0">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ $t('mt.topic.featured') }}</text>
|
||||
</view>
|
||||
<scroll-view direction="horizontal" class="featured-scroll" :scroll-x="true">
|
||||
<view class="featured-topics">
|
||||
<view
|
||||
v-for="(topic, index) in featuredTopicsList"
|
||||
:key="topic.id"
|
||||
class="featured-topic"
|
||||
@click="navigateToTopicDetail(topic)">
|
||||
<view class="topic-cover" :style="{ backgroundImage: `url(${topic.cover_image})` }">
|
||||
<view class="topic-overlay">
|
||||
<view class="topic-badge" :style="{ backgroundColor: getTopicStatusColor(topic.status) }">
|
||||
<text class="badge-text">{{ getTopicTypeDisplayName(topic.topic_type) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="topic-info">
|
||||
<text class="topic-title">{{ topic.title }}</text>
|
||||
<text class="topic-desc">{{ topic.description }}</text>
|
||||
<view class="topic-stats">
|
||||
<text class="stat-text">{{ topic.content_count }}篇文章</text>
|
||||
<text class="stat-text">{{ topic.view_count }}阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 专题列表 -->
|
||||
<view class="topics-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ $t('mt.topic.all') }}</text>
|
||||
<view class="section-actions">
|
||||
<view class="sort-btn" @click="showSortOptions">
|
||||
<text class="sort-text">{{ sortOptionText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-section" v-if="pageState.loading">
|
||||
<text class="loading-text">{{ $t('mt.status.loading') }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<view class="error-section" v-if="pageState.error !== null">
|
||||
<text class="error-text">{{ pageState.error }}</text>
|
||||
<view class="retry-btn" @click="retryLoad">
|
||||
<text class="retry-text">{{ $t('mt.action.retry') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 专题列表 -->
|
||||
<view class="topics-list" v-if="topicsList.length > 0">
|
||||
<view
|
||||
v-for="(topic, index) in topicsList"
|
||||
:key="topic.id"
|
||||
class="topic-item"
|
||||
@click="navigateToTopicDetail(topic)">
|
||||
<view class="topic-header">
|
||||
<view class="topic-type-badge" :style="{ backgroundColor: getTopicStatusColor(topic.status) }">
|
||||
<text class="type-badge-text">{{ getTopicTypeDisplayName(topic.topic_type) }}</text>
|
||||
</view>
|
||||
<text class="topic-time">{{ formatRelativeTimeKey(topic.updated_at) }}</text>
|
||||
</view>
|
||||
<text class="topic-title">{{ topic.title }}</text>
|
||||
<text class="topic-description">{{ topic.description }}</text>
|
||||
<view class="topic-meta">
|
||||
<view class="topic-stats">
|
||||
<text class="stat-item">📄 {{ topic.content_count }}{{ $t('mt.topic.articleCount') }}</text>
|
||||
<text class="stat-item">👁 {{ topic.view_count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-section" v-if="topicsList.length === 0 && !pageState.loading && pageState.error === null">
|
||||
<text class="empty-text">{{ $t('mt.topic.empty') }}</text>
|
||||
<view class="refresh-btn" @click="refreshData">
|
||||
<text class="refresh-text">{{ $t('mt.action.refresh') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more-section" v-if="topicsList.length > 0 && hasMore">
|
||||
<view class="load-more-btn" @click="loadMore" v-if="!loadingMore">
|
||||
<text class="load-more-text">{{ $t('mt.button.loadMore') }}</text>
|
||||
</view>
|
||||
<view class="loading-more" v-if="loadingMore">
|
||||
<text class="loading-more-text">{{ $t('mt.loadingMore') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 类型选择弹窗 -->
|
||||
<view class="modal-overlay" v-if="showTypeModal" @click="hideTypeSelector">
|
||||
<view class="type-modal" @click.stop="">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ $t('mt.topic.typeTitle') }}</text>
|
||||
<view class="modal-close" @click="hideTypeSelector">
|
||||
<text class="close-text">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="type-list">
|
||||
<view
|
||||
v-for="(type, index) in topicTypesList"
|
||||
:key="type.value"
|
||||
class="type-item"
|
||||
:class="{ active: selectedTypeValue === type.value }"
|
||||
@click="selectTopicType(type)">
|
||||
<text class="type-name">{{ type.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 排序选择弹窗 -->
|
||||
<view class="modal-overlay" v-if="showSortModal" @click="hideSortOptions">
|
||||
<view class="sort-modal" @click.stop="">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ $t('mt.modal.sort') }}</text>
|
||||
<view class="modal-close" @click="hideSortOptions">
|
||||
<text class="close-text">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="sort-list">
|
||||
<view
|
||||
v-for="(option, index) in sortOptionsList"
|
||||
:key="option.value"
|
||||
class="sort-item"
|
||||
:class="{ active: currentSortOption === option.value }"
|
||||
@click="selectSortOption(option)">
|
||||
<text class="sort-name">{{ option.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import {
|
||||
TopicData,
|
||||
PageState,
|
||||
ResponsiveState,
|
||||
TOPIC_TYPES,
|
||||
TOPIC_STATUS,
|
||||
SORT_OPTIONS,
|
||||
getTopicTypeDisplayName,
|
||||
getTopicStatusColor,
|
||||
formatRelativeTimeKey
|
||||
} from './types.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { tt } from '@/utils/i18nfun.uts'
|
||||
import i18n from '@/uni_modules/i18n/index.uts' // 保留用于语言切换
|
||||
|
||||
// 页面状态
|
||||
const pageState = ref<PageState>({
|
||||
loading: false,
|
||||
error: null,
|
||||
currentPage: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// UI状态变量
|
||||
const showTypeModal = ref<boolean>(false)
|
||||
const showSortModal = ref<boolean>(false)
|
||||
const loadingMore = ref<boolean>(false)
|
||||
const hasMore = ref<boolean>(true)
|
||||
|
||||
// 当前选择状态
|
||||
const selectedTypeValue = ref<string>('')
|
||||
const currentTypeText = ref<string>(tt('mt.topic.allTypes'))
|
||||
const currentSortOption = ref<string>('updated_at_desc')
|
||||
const sortOptionText = ref<string>(tt('mt.topic.sort.recentUpdate'))
|
||||
|
||||
// 数据列表 - 直接使用强类型 TopicData 数组
|
||||
const topicsList = ref<Array<TopicData>>([])
|
||||
const featuredTopicsList = ref<Array<TopicData>>([])
|
||||
|
||||
|
||||
// 选项列表
|
||||
const topicTypesList = ref([
|
||||
{ value: '', text: tt('mt.topic.allTypes') },
|
||||
...TOPIC_TYPES.map(type => ({
|
||||
value: type.value,
|
||||
text: tt(`mt.topicType.${type.value}`)
|
||||
}))
|
||||
])
|
||||
const sortOptionsList = ref([
|
||||
{ value: 'updated_at_desc', text: tt('mt.topic.sort.recentUpdate') },
|
||||
{ value: 'created_at_desc', text: tt('mt.topic.sort.newest') },
|
||||
{ value: 'view_count_desc', text: tt('mt.topic.sort.popular') },
|
||||
{ value: 'content_count_desc', text: tt('mt.topic.sort.contentCount') }
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const topicFilter = computed((): string => {
|
||||
let filter = "status=in.(active,featured)"
|
||||
if (selectedTypeValue.value !== '') {
|
||||
filter += `&topic_type=eq.${selectedTypeValue.value}`
|
||||
}
|
||||
|
||||
// 排序
|
||||
const sortParts = currentSortOption.value.split('_')
|
||||
const column = sortParts.slice(0, -1).join('_')
|
||||
const direction = sortParts[sortParts.length - 1] === 'desc' ? 'desc' : 'asc'
|
||||
filter += `&order=${column}.${direction}`
|
||||
return filter
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initializeData()
|
||||
})
|
||||
|
||||
// 初始化数据
|
||||
const initializeData = () => {
|
||||
loadTopics()
|
||||
loadFeaturedTopics()
|
||||
}
|
||||
|
||||
// 加载专题数据
|
||||
const loadTopics = async () => {
|
||||
if (pageState.value.loading) return
|
||||
pageState.value.loading = true
|
||||
pageState.value.error = null
|
||||
try {
|
||||
let query = supa.from('ak_topics')
|
||||
.select('*')
|
||||
.in('status', ['active', 'featured'])
|
||||
if (selectedTypeValue.value !== '') {
|
||||
query = query.eq('topic_type', selectedTypeValue.value)
|
||||
}
|
||||
const sortParts = currentSortOption.value.split('_')
|
||||
const column = sortParts.slice(0, -1).join('_')
|
||||
const direction = sortParts[sortParts.length - 1] === 'desc'
|
||||
if (direction) {
|
||||
query = query.order(column, { ascending: false })
|
||||
} else {
|
||||
query = query.order(column, { ascending: true })
|
||||
}
|
||||
const start = (pageState.value.currentPage - 1) * pageState.value.pageSize
|
||||
const end = start + pageState.value.pageSize - 1
|
||||
query = query.range(start, end)
|
||||
const result = await query.executeAs<Array<TopicData>>()
|
||||
if (result.error !== null) {
|
||||
throw new Error(result.error.message)
|
||||
}
|
||||
const data = result.data
|
||||
if (data !== null) {
|
||||
if (pageState.value.currentPage === 1) {
|
||||
topicsList.value = data
|
||||
} else {
|
||||
topicsList.value = topicsList.value.concat(data)
|
||||
}
|
||||
hasMore.value = data.length === pageState.value.pageSize
|
||||
}
|
||||
} catch (e: any) {
|
||||
pageState.value.error = tt('mt.error.loadTopicsFailed')
|
||||
console.error('Topics loading error:', e)
|
||||
} finally {
|
||||
pageState.value.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载精选专题
|
||||
const loadFeaturedTopics = async () => {
|
||||
try {
|
||||
const result = await supa.from('ak_topics')
|
||||
.select('*')
|
||||
.eq('status', 'featured')
|
||||
.order('updated_at', { ascending: false })
|
||||
.limit(5)
|
||||
.executeAs<Array<TopicData>>()
|
||||
if (result.error !== null) {
|
||||
console.error('Featured topics loading error:', result.error)
|
||||
return
|
||||
}
|
||||
const data = result.data
|
||||
if (data !== null) {
|
||||
featuredTopicsList.value = data
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Featured topics loading error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 类型选择
|
||||
const showTypeSelector = () => {
|
||||
showTypeModal.value = true
|
||||
}
|
||||
|
||||
const hideTypeSelector = () => {
|
||||
showTypeModal.value = false
|
||||
}
|
||||
|
||||
const selectTopicType = (type: any) => {
|
||||
selectedTypeValue.value = type.value
|
||||
currentTypeText.value = type.text
|
||||
hideTypeSelector()
|
||||
pageState.value.currentPage = 1
|
||||
loadTopics()
|
||||
}
|
||||
|
||||
// 排序选择
|
||||
const showSortOptions = () => {
|
||||
showSortModal.value = true
|
||||
}
|
||||
|
||||
const hideSortOptions = () => {
|
||||
showSortModal.value = false
|
||||
}
|
||||
|
||||
const selectSortOption = (option: any) => {
|
||||
currentSortOption.value = option.value
|
||||
sortOptionText.value = option.text
|
||||
hideSortOptions()
|
||||
pageState.value.currentPage = 1
|
||||
loadTopics()
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (loadingMore.value || !hasMore.value) return
|
||||
|
||||
loadingMore.value = true
|
||||
pageState.value.currentPage += 1
|
||||
|
||||
setTimeout(() => {
|
||||
loadTopics()
|
||||
loadingMore.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
pageState.value.currentPage = 1
|
||||
loadTopics()
|
||||
}
|
||||
|
||||
// 重试加载
|
||||
const retryLoad = () => {
|
||||
pageState.value.error = null
|
||||
loadTopics()
|
||||
}
|
||||
|
||||
// 导航函数
|
||||
const navigateToTopicDetail = (topic: TopicData) => {
|
||||
const topicId = topic.id
|
||||
uni.navigateTo({
|
||||
url: `/pages/info/topic-detail?id=${topicId}`
|
||||
})
|
||||
}
|
||||
|
||||
const navigateToSearch = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/info/search?type=topic'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.topics-page {
|
||||
flex: 1;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #ffffff;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #e2e8f0;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 12px;
|
||||
margin-left: 8px;
|
||||
background-color: #f1f5f9;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.type-section {
|
||||
background-color: #ffffff;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.type-scroll {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.type-tabs {
|
||||
flex-direction: row;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.type-tab {
|
||||
padding: 8px 16px;
|
||||
margin-right: 12px;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 20px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.type-tab.active {
|
||||
background-color: #8b5cf6;
|
||||
border-color: #8b5cf6;
|
||||
}
|
||||
|
||||
.type-text {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.type-tab.active .type-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.featured-section {
|
||||
background-color: #ffffff;
|
||||
margin-top: 8px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.section-actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sort-btn {
|
||||
padding: 6px 12px;
|
||||
background-color: #f1f5f9;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.sort-text {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.featured-scroll {
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.featured-topics {
|
||||
flex-direction: row;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.featured-topic {
|
||||
width: 280px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
margin-right: 12px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.topic-cover {
|
||||
height: 140px;
|
||||
background-color: #f1f5f9;
|
||||
border-top-left-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.topic-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 12px;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.topic-badge {
|
||||
background-color: #8b5cf6;
|
||||
border-radius: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.topic-info {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.topic-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
line-height: 22px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.topic-desc {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.topic-stats {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-text {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.topics-section {
|
||||
background-color: #ffffff;
|
||||
margin-top: 8px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.topics-list {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.topic-item {
|
||||
background-color: #f8fafc;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.topic-header {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.topic-type-badge {
|
||||
background-color: #8b5cf6;
|
||||
border-radius: 10px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.type-badge-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.topic-time {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.topic-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
line-height: 22px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.topic-description {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
line-height: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.topic-meta {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.topic-stats {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.loading-section, .error-section {
|
||||
padding: 40px 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
color: #ef4444;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.retry-btn, .refresh-btn {
|
||||
background-color: #8b5cf6;
|
||||
border-radius: 20px;
|
||||
padding: 8px 24px;
|
||||
}
|
||||
|
||||
.retry-text, .refresh-text {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.empty-section {
|
||||
padding: 60px 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.load-more-section {
|
||||
padding: 20px 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.load-more-btn {
|
||||
background-color: #f1f5f9;
|
||||
border-radius: 20px;
|
||||
padding: 10px 24px;
|
||||
border-width: 1px;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.load-more-text {
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.loading-more {
|
||||
padding: 10px 24px;
|
||||
}
|
||||
|
||||
.loading-more-text {
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.type-modal, .sort-modal {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16px;
|
||||
margin: 20px;
|
||||
max-height: 500px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 16px;
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
|
||||
.close-text {
|
||||
font-size: 20px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.type-list, .sort-list {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.type-item, .sort-item {
|
||||
padding: 16px 20px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #f1f5f9;
|
||||
}
|
||||
|
||||
.type-item.active, .sort-item.active {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.type-name, .sort-name {
|
||||
font-size: 16px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.type-item.active .type-name,
|
||||
.sort-item.active .sort-name {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
</style>
|
||||
708
mall/pages/info/types.uts
Normal file
708
mall/pages/info/types.uts
Normal file
@@ -0,0 +1,708 @@
|
||||
import { tt } from '@/utils/i18nfun.uts'
|
||||
|
||||
// 获取语言本地化 key(如 mt.language.zh-CN),用于 $t()
|
||||
export const getLanguageDisplayNameKey = (code: string): string => {
|
||||
if (code === 'zh-CN') return 'mt.language.zh-CN'
|
||||
if (code === 'zh-TW') return 'mt.language.zh-TW'
|
||||
if (code === 'en-US') return 'mt.language.en-US'
|
||||
if (code === 'ja-JP') return 'mt.language.ja-JP'
|
||||
if (code === 'ko-KR') return 'mt.language.ko-KR'
|
||||
if (code === 'fr-FR') return 'mt.language.fr-FR'
|
||||
if (code === 'de-DE') return 'mt.language.de-DE'
|
||||
if (code === 'es-ES') return 'mt.language.es-ES'
|
||||
return code
|
||||
}
|
||||
// 基础数据类型 - 全部使用强类型定义,便于类型推断和类型安全
|
||||
export type InfoContent = {
|
||||
id: string
|
||||
title?: string
|
||||
summary?: string | null
|
||||
content?: string
|
||||
author?: string
|
||||
trans_data?:TranslationData
|
||||
published_at?: string
|
||||
quality_score: number
|
||||
view_count?: number
|
||||
like_count?: number
|
||||
share_count?: number
|
||||
category_id?: string
|
||||
category_name?: CategoryTranslation
|
||||
category_name_text?: string // 兼容性字段,用于简单的字符串显示
|
||||
|
||||
original_language?: string
|
||||
source_url?: string | null
|
||||
tags?: string[] | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
is_liked?: boolean
|
||||
loading?: boolean // UI状态字段,用于加载状态显示
|
||||
|
||||
// 扩展字段 - 支持视频、音频和图集模式
|
||||
raw_content_id?: string | null
|
||||
keywords?: string[] | null
|
||||
entities?: any | null // jsonb
|
||||
sentiment_score?: number | null
|
||||
readability_score?: number | null
|
||||
credibility_score?: number | null
|
||||
comment_count?: number | null
|
||||
featured_until?: string | null
|
||||
status?: string | null
|
||||
ai_processed_at?: string | null
|
||||
favorite_count?: number | null
|
||||
is_featured?: boolean | null
|
||||
content_type?: string | null
|
||||
|
||||
// 视频相关字段
|
||||
video_url?: string | null
|
||||
video_duration?: number | null
|
||||
video_poster?: string | null
|
||||
video_width?: number | null
|
||||
video_height?: number | null
|
||||
video_size?: number | null
|
||||
video_format?: string | null
|
||||
video_quality?: string | null
|
||||
|
||||
// 音频相关字段
|
||||
audio_url?: string | null
|
||||
audio_duration?: number | null
|
||||
audio_size?: number | null
|
||||
audio_format?: string | null
|
||||
audio_bitrate?: number | null
|
||||
audio_sample_rate?: number | null
|
||||
audio_cover?: string | null
|
||||
|
||||
// 图片相关字段
|
||||
image_url?: string | null
|
||||
image_width?: number | null
|
||||
image_height?: number | null
|
||||
image_size?: number | null
|
||||
image_format?: string | null
|
||||
image_quality?: string | null
|
||||
image_alt_text?: string | null
|
||||
images?: any | null // jsonb - 图集模式
|
||||
|
||||
// 多媒体设置
|
||||
allow_danmu?: boolean | null
|
||||
allow_download?: boolean | null
|
||||
media_metadata?: any | null // jsonb
|
||||
|
||||
// 序列号字段
|
||||
cid?: number | null
|
||||
}
|
||||
|
||||
export type TranslationData = {
|
||||
id: string
|
||||
content_id: string
|
||||
language_id: string
|
||||
title: string
|
||||
content: string
|
||||
summary: string | null
|
||||
human_verified: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 新增:CategoryTranslation 类型
|
||||
export type CategoryTranslation = {
|
||||
name: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 新增:CategoryData 类型,严格对应 Supabase 返回结构
|
||||
export type CategoryData = {
|
||||
id: string
|
||||
name_key: string
|
||||
parent_id: string | null
|
||||
level?: number
|
||||
ai_keywords?: string[]
|
||||
confidence_threshold?: number
|
||||
sort_order?: number
|
||||
is_active?: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
translations?: CategoryTranslation[]
|
||||
}
|
||||
|
||||
// 新增专题相关类型
|
||||
export type TopicData = {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
created_by: string
|
||||
is_active: boolean
|
||||
content_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type TopicContentData = {
|
||||
id: string
|
||||
topic_id: string
|
||||
content_id: string
|
||||
display_order: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
is_liked?:boolean
|
||||
}
|
||||
|
||||
export type TopicTimelineData = {
|
||||
id: string
|
||||
topic_id: string
|
||||
event: string
|
||||
event_time: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 评论系统相关类型
|
||||
export type CommentData = {
|
||||
id: string
|
||||
content_id: string
|
||||
user_id: string
|
||||
user_name: string
|
||||
content: string
|
||||
like_count: number
|
||||
reply_count: number
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
is_liked?: boolean
|
||||
level?: number // 多级评论层级,0为主评论,1为一级回复,依此类推
|
||||
is_author?: boolean // 是否为当前用户本人评论
|
||||
}
|
||||
|
||||
export type CommentReplyData = {
|
||||
id: string
|
||||
comment_id: string
|
||||
user_id: string
|
||||
user_name: string
|
||||
content: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
is_liked?:boolean
|
||||
}
|
||||
|
||||
export type CommentReactionData = {
|
||||
id: string
|
||||
comment_id: string
|
||||
user_id: string
|
||||
reaction_type: string
|
||||
created_at: string
|
||||
is_liked?:boolean
|
||||
}
|
||||
|
||||
export type UserBehaviorData = {
|
||||
id: string
|
||||
user_id: string
|
||||
content_id: string
|
||||
behavior_type: string
|
||||
behavior_data: any
|
||||
duration_seconds: number | null
|
||||
scroll_percentage: number | null
|
||||
device_type: string
|
||||
source: string
|
||||
session_id: string
|
||||
ip_address: string
|
||||
user_agent: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type RecommendationData = {
|
||||
id: string
|
||||
user_id: string
|
||||
content_id: string
|
||||
algorithm_type: string
|
||||
score: number
|
||||
reason: string
|
||||
position: number
|
||||
shown_at: string
|
||||
clicked_at: string
|
||||
feedback_score: number
|
||||
feedback_reason: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type ChatSessionData = {
|
||||
id: string
|
||||
user_id: string
|
||||
session_name: string
|
||||
language: string
|
||||
context: any
|
||||
ai_model: string
|
||||
total_messages: number
|
||||
total_tokens: number
|
||||
cost_usd: number
|
||||
last_message_at: string
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type ChatMessageData = {
|
||||
id: string
|
||||
session_id: string
|
||||
message_type: string
|
||||
content: string
|
||||
intent?: string
|
||||
attachments?: any
|
||||
ai_provider?: string
|
||||
tokens_used?: number
|
||||
processing_time_ms?: number
|
||||
cost_usd?: number
|
||||
feedback_score?: number
|
||||
feedback_reason?: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
export type LanguageData = {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
native_name: string
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
export type UserSettingsData = {
|
||||
id: string
|
||||
user_id: string
|
||||
preferred_languages: string[]
|
||||
preferred_categories: string[]
|
||||
reading_mode: string
|
||||
font_size: string
|
||||
auto_translate: boolean
|
||||
notification_enabled: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type SearchHistoryData = {
|
||||
id: string
|
||||
user_id: string
|
||||
keyword: string
|
||||
searched_at: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 已合并到 TranslationData,避免重复定义
|
||||
// export type Translation = TranslationData
|
||||
|
||||
export type Topic = {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
topic_type?: string
|
||||
status?: string
|
||||
cover_image?: string
|
||||
created_by?: string
|
||||
is_active?: boolean
|
||||
content_count?: number
|
||||
view_count?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export type Comment = {
|
||||
id: string
|
||||
content_id: string
|
||||
user_id: string
|
||||
user_name: string
|
||||
content: string
|
||||
like_count: number
|
||||
reply_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type Language = {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
native_name: string
|
||||
is_active: boolean
|
||||
}
|
||||
|
||||
// 状态和UI类型 - 与template交互的变量使用1维变量
|
||||
export type PageState = {
|
||||
loading: boolean
|
||||
error: string | null
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type StatsData = {
|
||||
total_contents: number
|
||||
published_contents: number
|
||||
trending_contents: number
|
||||
avg_quality_score: string
|
||||
}
|
||||
|
||||
export type ResponsiveState = {
|
||||
isLargeScreen: boolean
|
||||
isSmallScreen: boolean
|
||||
screenWidth: number
|
||||
cardColumns: number
|
||||
}
|
||||
|
||||
// 选择器选项类型 - UTS Android支持的简单类型
|
||||
export type PickerOption = {
|
||||
value: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export type SortOption = {
|
||||
column: string
|
||||
ascending: boolean
|
||||
}
|
||||
|
||||
// 表单数据类型 - 避免复杂嵌套,使用简单类型
|
||||
export type ContentFormData = {
|
||||
title: string
|
||||
content: string
|
||||
summary: string
|
||||
category_id: string
|
||||
tags: string // 改为字符串,用逗号分隔
|
||||
source_url: string
|
||||
author: string
|
||||
content_type?: string
|
||||
|
||||
// 视频相关字段
|
||||
video_url?: string
|
||||
video_duration?: number
|
||||
video_poster?: string
|
||||
video_width?: number
|
||||
video_height?: number
|
||||
video_quality?: string
|
||||
|
||||
// 音频相关字段
|
||||
audio_url?: string
|
||||
audio_duration?: number
|
||||
audio_cover?: string
|
||||
audio_format?: string
|
||||
|
||||
// 图片相关字段
|
||||
image_url?: string
|
||||
image_alt_text?: string
|
||||
images?: string // JSON字符串存储图集数据
|
||||
|
||||
// 多媒体设置
|
||||
allow_danmu?: boolean
|
||||
allow_download?: boolean
|
||||
}
|
||||
|
||||
export type TranslationFormData = {
|
||||
content_id: string
|
||||
language_id: string
|
||||
title: string
|
||||
content: string
|
||||
summary: string
|
||||
}
|
||||
|
||||
// 筛选器类型 - 使用简单字符串类型
|
||||
export type ContentFilterData = {
|
||||
category_id: string | null
|
||||
language: string | null
|
||||
status: string
|
||||
quality_min: string | null
|
||||
date_from: string | null
|
||||
date_to: string | null
|
||||
search_text: string | null
|
||||
date_range?: string | null
|
||||
content_type?: string | null // 新增:按内容类型筛选
|
||||
is_featured?: boolean | null // 新增:是否精选
|
||||
has_video?: boolean | null // 新增:是否包含视频
|
||||
has_audio?: boolean | null // 新增:是否包含音频
|
||||
has_images?: boolean | null // 新增:是否包含图片
|
||||
}
|
||||
|
||||
// 聊天相关类型
|
||||
export type ChatState = {
|
||||
isTyping: boolean
|
||||
currentSession: string |null
|
||||
messageCount: number
|
||||
}
|
||||
|
||||
// 用户偏好类型 - 使用字符串存储数组数据
|
||||
export type UserPreferences = {
|
||||
preferred_languages: string // JSON字符串存储数组
|
||||
preferred_categories: string // JSON字符串存储数组
|
||||
reading_mode: string // 'light', 'dark', 'auto'
|
||||
font_size: string // 'small', 'medium', 'large'
|
||||
auto_translate: boolean
|
||||
notification_enabled: boolean
|
||||
}
|
||||
|
||||
|
||||
// 常量定义 - 内容状态
|
||||
export const CONTENT_STATUS = {
|
||||
DRAFT: 'draft',
|
||||
PUBLISHED: 'published',
|
||||
ARCHIVED: 'archived',
|
||||
DELETED: 'deleted'
|
||||
}
|
||||
|
||||
// 内容类型常量 - 支持多媒体
|
||||
export const CONTENT_TYPES = {
|
||||
TEXT: 'text', // 纯文本
|
||||
IMAGE: 'image', // 图片
|
||||
VIDEO: 'video', // 视频
|
||||
AUDIO: 'audio', // 音频
|
||||
GALLERY: 'gallery', // 图集
|
||||
MIXED: 'mixed' // 混合内容
|
||||
}
|
||||
|
||||
// 通用选项类型(用于 value/text 结构的所有 option)
|
||||
export type OptionItem = {
|
||||
value: string
|
||||
text: string
|
||||
}
|
||||
|
||||
// 视频质量选项
|
||||
export const VIDEO_QUALITY_OPTIONS: Array<OptionItem> = [
|
||||
{ value: '4K', text: 'mt.video.quality.4k' },
|
||||
{ value: '1080P', text: 'mt.video.quality.1080p' },
|
||||
{ value: '720P', text: 'mt.video.quality.720p' },
|
||||
{ value: '480P', text: 'mt.video.quality.480p' },
|
||||
{ value: '360P', text: 'mt.video.quality.360p' }
|
||||
]
|
||||
|
||||
// 音频格式选项
|
||||
export const AUDIO_FORMAT_OPTIONS: Array<OptionItem> = [
|
||||
{ value: 'mp3', text: 'mt.audio.format.mp3' },
|
||||
{ value: 'wav', text: 'mt.audio.format.wav' },
|
||||
{ value: 'flac', text: 'mt.audio.format.flac' },
|
||||
{ value: 'aac', text: 'mt.audio.format.aac' },
|
||||
{ value: 'm4a', text: 'mt.audio.format.m4a' }
|
||||
]
|
||||
|
||||
// 行为类型常量
|
||||
export const BEHAVIOR_TYPES = {
|
||||
VIEW: 'view',
|
||||
LIKE: 'like',
|
||||
SHARE: 'share',
|
||||
COMMENT: 'comment',
|
||||
SAVE: 'save',
|
||||
CLICK: 'click'
|
||||
}
|
||||
|
||||
// 消息类型常量
|
||||
export const MESSAGE_TYPES = {
|
||||
USER: 'user',
|
||||
ASSISTANT: 'assistant',
|
||||
SYSTEM: 'system'
|
||||
}
|
||||
|
||||
// 专题类型常量
|
||||
export const TOPIC_TYPES: Array<OptionItem> = [
|
||||
{ value: 'breaking', text: 'mt.topicType.breaking' },
|
||||
{ value: 'trending', text: 'mt.topicType.trending' },
|
||||
{ value: 'series', text: 'mt.topicType.series' },
|
||||
{ value: 'analysis', text: 'mt.topicType.analysis' },
|
||||
{ value: 'guide', text: 'mt.topicType.guide' },
|
||||
{ value: 'interview', text: 'mt.topicType.interview' },
|
||||
{ value: 'report', text: 'mt.topicType.report' },
|
||||
{ value: 'timeline', text: 'mt.topicType.timeline' }
|
||||
]
|
||||
|
||||
// 专题状态常量
|
||||
export const TOPIC_STATUS = {
|
||||
DRAFT: 'draft',
|
||||
ACTIVE: 'active',
|
||||
FEATURED: 'featured',
|
||||
ARCHIVED: 'archived',
|
||||
CLOSED: 'closed'
|
||||
}
|
||||
|
||||
// 评论状态常量
|
||||
export const COMMENT_STATUS = {
|
||||
ACTIVE: 'active',
|
||||
HIDDEN: 'hidden',
|
||||
DELETED: 'deleted',
|
||||
PENDING_REVIEW: 'pending_review',
|
||||
REJECTED: 'rejected'
|
||||
}
|
||||
|
||||
// 评论类型常量
|
||||
export const COMMENT_TYPES = {
|
||||
CONTENT: 'content', // 内容评论
|
||||
TOPIC: 'topic', // 专题评论
|
||||
REPLY: 'reply' // 回复评论
|
||||
}
|
||||
|
||||
// 评论排序选项
|
||||
export const COMMENT_SORT_OPTIONS: Array<OptionItem> = [
|
||||
{ value: 'created_at_desc', text: 'mt.comment.sort.latest' },
|
||||
{ value: 'created_at_asc', text: 'mt.comment.sort.earliest' },
|
||||
{ value: 'like_count_desc', text: 'mt.comment.sort.mostLiked' },
|
||||
{ value: 'reply_count_desc', text: 'mt.comment.sort.mostReplied' }
|
||||
]
|
||||
|
||||
// 评论举报类型
|
||||
export const COMMENT_REPORT_TYPES: Array<OptionItem> = [
|
||||
{ value: 'spam', text: 'mt.comment.report.spam' },
|
||||
{ value: 'inappropriate', text: 'mt.comment.report.inappropriate' },
|
||||
{ value: 'harassment', text: 'mt.comment.report.harassment' },
|
||||
{ value: 'misinformation', text: 'mt.comment.report.misinformation' },
|
||||
{ value: 'copyright', text: 'mt.comment.report.copyright' },
|
||||
{ value: 'other', text: 'mt.comment.report.other' }
|
||||
]
|
||||
|
||||
// 语言选项常量
|
||||
export const LANGUAGE_OPTIONS: Array<LanguageData> = [
|
||||
{ id: 'zh-CN', code: 'zh-CN', name: 'mt.language.zh-CN', native_name: 'mt.language.zh-CN', is_active: true },
|
||||
{ id: 'zh-TW', code: 'zh-TW', name: 'mt.language.zh-TW', native_name: 'mt.language.zh-TW', is_active: true },
|
||||
{ id: 'en-US', code: 'en-US', name: 'mt.language.en-US', native_name: 'mt.language.en-US', is_active: true },
|
||||
{ id: 'ja-JP', code: 'ja-JP', name: 'mt.language.ja-JP', native_name: 'mt.language.ja-JP', is_active: true },
|
||||
{ id: 'ko-KR', code: 'ko-KR', name: 'mt.language.ko-KR', native_name: 'mt.language.ko-KR', is_active: true },
|
||||
{ id: 'fr-FR', code: 'fr-FR', name: 'mt.language.fr-FR', native_name: 'mt.language.fr-FR', is_active: true },
|
||||
{ id: 'de-DE', code: 'de-DE', name: 'mt.language.de-DE', native_name: 'mt.language.de-DE', is_active: true },
|
||||
{ id: 'es-ES', code: 'es-ES', name: 'mt.language.es-ES', native_name: 'mt.language.es-ES', is_active: true }
|
||||
]
|
||||
export const SORT_OPTIONS: Array<OptionItem> = [
|
||||
{ value: 'published_at_desc', text: 'mt.sort.latest' },
|
||||
{ value: 'published_at_asc', text: 'mt.sort.earliest' },
|
||||
{ value: 'quality_score_desc', text: 'mt.sort.highestScore' },
|
||||
{ value: 'view_count_desc', text: 'mt.sort.mostViewed' },
|
||||
{ value: 'like_count_desc', text: 'mt.sort.mostLiked' },
|
||||
{ value: 'share_count_desc', text: 'mt.sort.mostShared' }
|
||||
]
|
||||
|
||||
|
||||
export const getCommentStatusTextKey = (status: string): string => {
|
||||
console.log(status,COMMENT_STATUS.HIDDEN)
|
||||
if (status === COMMENT_STATUS["ACTIVE"]) return 'mt.comment.status.active'
|
||||
if (status === COMMENT_STATUS.HIDDEN) return 'mt.comment.status.hidden'
|
||||
if (status === COMMENT_STATUS.DELETED) return 'mt.comment.status.deleted'
|
||||
if (status === COMMENT_STATUS.PENDING_REVIEW) return 'mt.comment.status.pending'
|
||||
if (status === COMMENT_STATUS.REJECTED) return 'mt.comment.status.rejected'
|
||||
return 'mt.comment.status.unknown'
|
||||
}
|
||||
|
||||
// 格式化相对时间,返回 i18n key
|
||||
export const formatRelativeTimeKey = (dateString: string | null): string => {
|
||||
if (dateString == null || dateString === '') return ''
|
||||
const now = new Date()
|
||||
const date = new Date(dateString)
|
||||
const diff = now.getTime() - date.getTime()
|
||||
const seconds = Math.floor(diff / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
if (days > 0) return days + tt('mt.time.daysAgo')
|
||||
if (hours > 0) return hours + tt('mt.time.hoursAgo')
|
||||
if (minutes > 0) return minutes + tt('mt.time.minutesAgo')
|
||||
return tt('mt.time.justNow')
|
||||
}
|
||||
|
||||
|
||||
// 语言显示名称,返回本地化字符串
|
||||
export const getLanguageDisplayName = (code: string): string => {
|
||||
const map = {
|
||||
'zh-CN': 'mt.language.zh-CN',
|
||||
'zh-TW': 'mt.language.zh-TW',
|
||||
'en-US': 'mt.language.en-US',
|
||||
'ja-JP': 'mt.language.ja-JP',
|
||||
'ko-KR': 'mt.language.ko-KR',
|
||||
'fr-FR': 'mt.language.fr-FR',
|
||||
'de-DE': 'mt.language.de-DE',
|
||||
'es-ES': 'mt.language.es-ES'
|
||||
}
|
||||
const key = map[code] ?? code
|
||||
return tt(key)
|
||||
}
|
||||
|
||||
// 质量分数对应颜色,返回颜色字符串(如需 className 可调整)
|
||||
export const getQualityScoreColor = (score: number): string => {
|
||||
if (score >= 90) return '#4CAF50' // excellent - green
|
||||
if (score >= 75) return '#8BC34A' // good - light green
|
||||
if (score >= 60) return '#FFC107' // normal - amber
|
||||
return '#F44336' // poor - red
|
||||
}
|
||||
|
||||
// 质量分数对应文本,返回 i18n key
|
||||
export const getQualityScoreText = (score: number): string => {
|
||||
if (score >= 90) return tt('mt.quality.excellent')
|
||||
if (score >= 75) return tt('mt.quality.good')
|
||||
if (score >= 60) return tt('mt.quality.normal')
|
||||
return tt('mt.quality.poor')
|
||||
}
|
||||
|
||||
// 专题类型显示名称,返回本地化字符串
|
||||
export const getTopicTypeDisplayName = (typeCode: string): string => {
|
||||
const typeItem = TOPIC_TYPES.find(item => item.value === typeCode);
|
||||
return typeItem != null ? tt(typeItem.text) : typeCode;
|
||||
};
|
||||
|
||||
// 专题状态对应颜色,返回颜色字符串
|
||||
export const getTopicStatusColor = (status: string): string => {
|
||||
if (status === TOPIC_STATUS.FEATURED) return '#FF6B35' // featured - orange
|
||||
if (status === TOPIC_STATUS.ACTIVE) return '#4CAF50' // active - green
|
||||
if (status === TOPIC_STATUS.DRAFT) return '#9E9E9E' // draft - gray
|
||||
if (status === TOPIC_STATUS.ARCHIVED) return '#607D8B' // archived - blue gray
|
||||
if (status === TOPIC_STATUS.CLOSED) return '#F44336' // closed - red
|
||||
return '#9E9E9E' // default - gray
|
||||
}
|
||||
|
||||
// 获取内容类型显示名称,返回本地化字符串
|
||||
export const getContentTypeDisplayName = (contentType: string | null): string => {
|
||||
if (contentType === CONTENT_TYPES.TEXT) return tt('mt.content.type.text')
|
||||
if (contentType === CONTENT_TYPES.IMAGE) return tt('mt.content.type.image')
|
||||
if (contentType === CONTENT_TYPES.VIDEO) return tt('mt.content.type.video')
|
||||
if (contentType === CONTENT_TYPES.AUDIO) return tt('mt.content.type.audio')
|
||||
if (contentType === CONTENT_TYPES.GALLERY) return tt('mt.content.type.gallery')
|
||||
if (contentType === CONTENT_TYPES.MIXED) return tt('mt.content.type.mixed')
|
||||
return tt('mt.content.type.text') // 默认为文本
|
||||
}
|
||||
|
||||
// 获取内容类型对应图标
|
||||
export const getContentTypeIcon = (contentType: string | null): string => {
|
||||
if (contentType === CONTENT_TYPES.TEXT) return 'text-outline'
|
||||
if (contentType === CONTENT_TYPES.IMAGE) return 'image-outline'
|
||||
if (contentType === CONTENT_TYPES.VIDEO) return 'videocam-outline'
|
||||
if (contentType === CONTENT_TYPES.AUDIO) return 'volume-high-outline'
|
||||
if (contentType === CONTENT_TYPES.GALLERY) return 'images-outline'
|
||||
if (contentType === CONTENT_TYPES.MIXED) return 'layers-outline'
|
||||
return 'text-outline' // 默认图标
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
export const formatFileSize = (bytes: number | null): string => {
|
||||
if (bytes == null || bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 格式化时长(秒转为 mm:ss 或 hh:mm:ss)
|
||||
export const formatDuration = (seconds: number | null): string => {
|
||||
if (seconds == null || seconds === 0) return '00:00'
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const remainingSeconds = Math.floor(seconds % 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`
|
||||
} else {
|
||||
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
|
||||
// 检查内容是否有多媒体
|
||||
export const hasMultimedia = (content: InfoContent): boolean => {
|
||||
return !!(content.video_url || content.audio_url || content.image_url || (content.images && content.images !== null))
|
||||
}
|
||||
|
||||
// 获取主要媒体类型
|
||||
export const getPrimaryMediaType = (content: InfoContent): string => {
|
||||
if (content.video_url) return CONTENT_TYPES.VIDEO
|
||||
if (content.audio_url) return CONTENT_TYPES.AUDIO
|
||||
if (content.images && content.images !== null) return CONTENT_TYPES.GALLERY
|
||||
if (content.image_url) return CONTENT_TYPES.IMAGE
|
||||
return CONTENT_TYPES.TEXT
|
||||
}
|
||||
|
||||
1204
mall/pages/info/video-player.uvue
Normal file
1204
mall/pages/info/video-player.uvue
Normal file
File diff suppressed because it is too large
Load Diff
277
mall/pages/info/video-types.uts
Normal file
277
mall/pages/info/video-types.uts
Normal file
@@ -0,0 +1,277 @@
|
||||
import { tt } from '@/utils/i18nfun.uts'
|
||||
|
||||
// 视频内容类型扩展
|
||||
export type VideoContent = {
|
||||
id: string
|
||||
title: string
|
||||
summary: string | null
|
||||
content: string
|
||||
author: string
|
||||
published_at: string
|
||||
quality_score: number
|
||||
category_id: string
|
||||
original_language: string
|
||||
source_url: string | null
|
||||
tags: string[] | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
|
||||
// 视频特有字段
|
||||
content_type: 'video'
|
||||
video_url: string
|
||||
video_duration: number
|
||||
video_poster: string
|
||||
video_width: number
|
||||
video_height: number
|
||||
video_size: number
|
||||
video_format: string
|
||||
video_quality: string
|
||||
allow_danmu: boolean
|
||||
allow_download: boolean
|
||||
|
||||
// 统计数据
|
||||
view_count: number
|
||||
like_count: number
|
||||
favorite_count: number
|
||||
share_count: number
|
||||
comment_count: number
|
||||
danmu_count: number
|
||||
play_completion_rate: number
|
||||
average_play_duration: number
|
||||
|
||||
// 用户状态
|
||||
is_liked?: boolean
|
||||
is_favorited?: boolean
|
||||
}
|
||||
|
||||
// 弹幕类型
|
||||
export type DanmuData = {
|
||||
id: string
|
||||
content_id: string
|
||||
user_id: string
|
||||
user_name: string
|
||||
text: string
|
||||
time_point: number
|
||||
color: string
|
||||
font_size: number
|
||||
position_type: 'scroll' | 'top' | 'bottom'
|
||||
speed: number
|
||||
is_visible: boolean
|
||||
status: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 弹幕发送数据
|
||||
export type DanmuSendData = {
|
||||
text: string
|
||||
time_point: number
|
||||
color?: string
|
||||
font_size?: number
|
||||
position_type?: 'scroll' | 'top' | 'bottom'
|
||||
speed?: number
|
||||
}
|
||||
|
||||
// 用户交互类型
|
||||
export type UserInteraction = {
|
||||
id: string
|
||||
user_id: string
|
||||
content_id: string
|
||||
interaction_type: 'like' | 'favorite' | 'share' | 'view' | 'download'
|
||||
interaction_data?: any
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 视频评论类型
|
||||
export type VideoComment = {
|
||||
id: string
|
||||
content_id: string
|
||||
user_id: string
|
||||
user_name: string
|
||||
parent_id: string | null
|
||||
reply_to_user_id: string | null
|
||||
reply_to_user_name: string | null
|
||||
content: string
|
||||
like_count: number
|
||||
reply_count: number
|
||||
status: string
|
||||
is_pinned: boolean
|
||||
level: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
is_liked_by_user?: boolean
|
||||
}
|
||||
|
||||
// 播放记录类型
|
||||
export type PlayRecord = {
|
||||
id: string
|
||||
content_id: string
|
||||
user_id: string
|
||||
play_position: number
|
||||
play_duration: number
|
||||
play_percentage: number
|
||||
is_completed: boolean
|
||||
device_type: string
|
||||
resolution: string
|
||||
play_speed: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 视频页面状态
|
||||
export type VideoPageState = {
|
||||
loading: boolean
|
||||
error: string | null
|
||||
danmu_loading: boolean
|
||||
comment_loading: boolean
|
||||
sending_danmu: boolean
|
||||
posting_comment: boolean
|
||||
}
|
||||
|
||||
// 弹幕配置
|
||||
export type DanmuConfig = {
|
||||
enabled: boolean
|
||||
opacity: number
|
||||
font_size: number
|
||||
speed: number
|
||||
show_area: number // 显示区域百分比
|
||||
max_count: number // 同时显示最大数量
|
||||
filter_enabled: boolean // 是否开启弹幕过滤
|
||||
filter_keywords: string[] // 过滤关键词
|
||||
}
|
||||
|
||||
// 视频播放器状态
|
||||
export type VideoPlayerState = {
|
||||
playing: boolean
|
||||
current_time: number
|
||||
duration: number
|
||||
volume: number
|
||||
playback_rate: number
|
||||
fullscreen: boolean
|
||||
quality: string
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
// 分享选项
|
||||
export type ShareOption = {
|
||||
platform: string
|
||||
name: string
|
||||
icon: string
|
||||
color: string
|
||||
}
|
||||
|
||||
// 视频质量选项
|
||||
export const VIDEO_QUALITY_OPTIONS = [
|
||||
{ value: '360p', text: 'mt.video.quality.360p' },
|
||||
{ value: '480p', text: 'mt.video.quality.480p' },
|
||||
{ value: '720p', text: 'mt.video.quality.720p' },
|
||||
{ value: '1080p', text: 'mt.video.quality.1080p' },
|
||||
{ value: '4k', text: 'mt.video.quality.4k' }
|
||||
]
|
||||
|
||||
// 播放速度选项
|
||||
export const PLAYBACK_RATE_OPTIONS = [
|
||||
{ value: 0.5, text: '0.5x' },
|
||||
{ value: 0.75, text: '0.75x' },
|
||||
{ value: 1.0, text: 'mt.video.speed.normal' },
|
||||
{ value: 1.25, text: '1.25x' },
|
||||
{ value: 1.5, text: '1.5x' },
|
||||
{ value: 2.0, text: '2.0x' }
|
||||
]
|
||||
|
||||
// 弹幕位置选项
|
||||
export const DANMU_POSITION_OPTIONS = [
|
||||
{ value: 'scroll', text: 'mt.video.danmu.position.scroll' },
|
||||
{ value: 'top', text: 'mt.video.danmu.position.top' },
|
||||
{ value: 'bottom', text: 'mt.video.danmu.position.bottom' }
|
||||
]
|
||||
|
||||
// 弹幕颜色选项
|
||||
export const DANMU_COLOR_OPTIONS = [
|
||||
'#FFFFFF', '#FF0000', '#00FF00', '#0000FF', '#FFFF00',
|
||||
'#FF00FF', '#00FFFF', '#FFA500', '#FFC0CB', '#800080'
|
||||
]
|
||||
|
||||
// 分享平台选项
|
||||
export const SHARE_PLATFORM_OPTIONS: ShareOption[] = [
|
||||
{ platform: 'wechat', name: 'mt.share.wechat', icon: '💬', color: '#07C160' },
|
||||
{ platform: 'weibo', name: 'mt.share.weibo', icon: '📱', color: '#E6162D' },
|
||||
{ platform: 'qq', name: 'mt.share.qq', icon: '🐧', color: '#12B7F5' },
|
||||
{ platform: 'link', name: 'mt.share.copyLink', icon: '🔗', color: '#666666' }
|
||||
]
|
||||
|
||||
// 工具函数
|
||||
|
||||
// 格式化视频时长
|
||||
export const formatVideoDuration = (seconds: number): string => {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const secs = Math.floor(seconds % 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||
} else {
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
export const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 格式化播放次数
|
||||
export const formatViewCount = (count: number): string => {
|
||||
if (count < 1000) return count.toString()
|
||||
if (count < 10000) return (count / 1000).toFixed(1) + 'K'
|
||||
if (count < 1000000) return Math.floor(count / 10000) + tt('mt.video.unit.wan')
|
||||
return (count / 1000000).toFixed(1) + 'M'
|
||||
}
|
||||
|
||||
// 获取视频质量显示文本
|
||||
export const getVideoQualityText = (quality: string): string => {
|
||||
const option = VIDEO_QUALITY_OPTIONS.find(opt => opt.value === quality)
|
||||
return option ? tt(option.text) : quality
|
||||
}
|
||||
|
||||
// 获取弹幕位置显示文本
|
||||
export const getDanmuPositionText = (position: string): string => {
|
||||
const option = DANMU_POSITION_OPTIONS.find(opt => opt.value === position)
|
||||
return option ? tt(option.text) : position
|
||||
}
|
||||
|
||||
// 获取分享平台显示文本
|
||||
export const getSharePlatformText = (platform: string): string => {
|
||||
const option = SHARE_PLATFORM_OPTIONS.find(opt => opt.platform === platform)
|
||||
return option ? tt(option.name) : platform
|
||||
}
|
||||
|
||||
// 弹幕校验结果类型
|
||||
export type DanmuValidateResult = { valid: boolean; error?: string };
|
||||
|
||||
// 验证弹幕内容
|
||||
export function validateDanmuText(text: string): DanmuValidateResult {
|
||||
if ( text.trim().length === 0) {
|
||||
return { valid: false, error: tt('mt.video.danmu.error.empty') };
|
||||
}
|
||||
if (text.length > 100) {
|
||||
return { valid: false, error: tt('mt.video.danmu.error.tooLong') };
|
||||
}
|
||||
// 检查是否包含敏感词
|
||||
const sensitiveWords = ['spam', 'advertisement']; // 简化示例
|
||||
const hasSensitive = sensitiveWords.some(word => text.toLowerCase().includes(word));
|
||||
if (hasSensitive) {
|
||||
return { valid: false, error: tt('mt.video.danmu.error.sensitive') };
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// 计算弹幕显示时间
|
||||
export const calculateDanmuDisplayTime = (textLength: number, speed: number): number => {
|
||||
// 基础显示时间 + 文本长度影响 / 速度
|
||||
return Math.max(3, (5 + textLength * 0.1) / speed)
|
||||
}
|
||||
137
mall/pages/llm/asr.uvue
Normal file
137
mall/pages/llm/asr.uvue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<view class="content">
|
||||
<view class="header">
|
||||
<text class="title">Sherpa-onnx ASR 调试</text>
|
||||
</view>
|
||||
|
||||
<view class="controls">
|
||||
<button class="btn" @click="init">1. 初始化模型</button>
|
||||
<button class="btn primary" @click="start">2. 开始录音</button>
|
||||
<button class="btn danger" @click="stop">3. 停止录音</button>
|
||||
</view>
|
||||
|
||||
<view class="result-box">
|
||||
<text class="label">识别结果:</text>
|
||||
<text class="result-text">{{ resultText }}</text>
|
||||
</view>
|
||||
|
||||
<view class="log-box">
|
||||
<text class="log-text">{{ logText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { initModel,requestAudioPermission,startRecording,stopRecording,disposeModel } from '../../uni_modules/ak-onnx/utssdk/app-android';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
resultText: "等待录音...",
|
||||
logText: "准备就绪"
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.log("页面加载,请求录音权限...");
|
||||
// #ifdef APP-ANDROID
|
||||
requestAudioPermission((granted: boolean) => {
|
||||
if (granted) {
|
||||
this.log("录音权限已授予");
|
||||
} else {
|
||||
this.log("录音权限被拒绝,请在设置中手动开启");
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
onUnload() {
|
||||
disposeModel
|
||||
},
|
||||
methods: {
|
||||
log(msg: string) {
|
||||
console.log(msg);
|
||||
this.logText = msg + "\n" + this.logText;
|
||||
},
|
||||
init() {
|
||||
this.log("正在初始化模型...");
|
||||
// 异步执行以防阻塞 UI
|
||||
setTimeout(() => {
|
||||
try {
|
||||
initModel();
|
||||
this.log("模型初始化成功!");
|
||||
} catch (e) {
|
||||
this.log("初始化失败: " + (e as Error).message);
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
start() {
|
||||
this.log("开始录音...");
|
||||
this.resultText = "";
|
||||
startRecording((text: string) => {
|
||||
this.resultText = text;
|
||||
});
|
||||
},
|
||||
stop() {
|
||||
stopRecording();
|
||||
this.log("录音已停止");
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
.primary {
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
}
|
||||
.danger {
|
||||
background-color: #dd524d;
|
||||
color: white;
|
||||
}
|
||||
.result-box {
|
||||
padding: 15px;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 8px;
|
||||
min-height: 100px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.result-text {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
.log-box {
|
||||
padding: 10px;
|
||||
background-color: #333;
|
||||
border-radius: 5px;
|
||||
min-height: 100px;
|
||||
}
|
||||
.log-text {
|
||||
font-size: 12px;
|
||||
color: #0f0;
|
||||
}
|
||||
</style>
|
||||
185
mall/pages/mall/admin/article/article.uts
Normal file
185
mall/pages/mall/admin/article/article.uts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 文章管理服务层
|
||||
* 提供文章列表、详情、保存、删除等接口
|
||||
*/
|
||||
|
||||
// 文章列表项数据结构
|
||||
export interface ArticleItem {
|
||||
id: number
|
||||
title: string
|
||||
category_id: number
|
||||
category_name: string
|
||||
image: string
|
||||
description: string
|
||||
status: number // 0: 未发布, 1: 已发布
|
||||
views: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 文章详情数据结构
|
||||
export interface ArticleDetail {
|
||||
id: number
|
||||
title: string
|
||||
category_id: number
|
||||
image: string
|
||||
description: string
|
||||
content: string
|
||||
status: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 文章创建/编辑参数
|
||||
export interface ArticlePayload {
|
||||
title: string
|
||||
category_id: number
|
||||
image: string
|
||||
description: string
|
||||
content: string
|
||||
status: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章列表
|
||||
* @param params 查询参数 { page, limit, keyword, status, category_id }
|
||||
* @returns Promise<{ items: ArticleItem[], total: number }>
|
||||
*/
|
||||
export function getArticleList(params: any = {}): Promise<any> {
|
||||
// TODO: 替换为实际 API 调用
|
||||
// return uni.$http.get('/article/list', { params })
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
title: '如何选择合适的商品分类',
|
||||
category_id: 1,
|
||||
category_name: '运营指南',
|
||||
image: '/static/article-1.png',
|
||||
description: '商品分类是电商平台的重要组成部分...',
|
||||
status: 1,
|
||||
views: 128,
|
||||
created_at: '2026-01-28 10:30:00',
|
||||
updated_at: '2026-01-28 10:30:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '商城营销活动最佳实践',
|
||||
category_id: 2,
|
||||
category_name: '营销技巧',
|
||||
image: '/static/article-2.png',
|
||||
description: '分享最新的营销活动策略和案例...',
|
||||
status: 1,
|
||||
views: 256,
|
||||
created_at: '2026-01-27 15:20:00',
|
||||
updated_at: '2026-01-27 15:20:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '用户评价管理指南',
|
||||
category_id: 1,
|
||||
category_name: '运营指南',
|
||||
image: '/static/article-3.png',
|
||||
description: '如何有效管理用户的评价和反馈...',
|
||||
status: 0,
|
||||
views: 64,
|
||||
created_at: '2026-01-26 09:15:00',
|
||||
updated_at: '2026-01-26 09:15:00'
|
||||
}
|
||||
],
|
||||
total: 3
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章详情
|
||||
* @param id 文章ID
|
||||
* @returns Promise<ArticleDetail>
|
||||
*/
|
||||
export function getArticleDetail(id: number): Promise<ArticleDetail> {
|
||||
// TODO: 替换为实际 API 调用
|
||||
// return uni.$http.get(`/article/${id}`)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
id,
|
||||
title: '如何选择合适的商品分类',
|
||||
category_id: 1,
|
||||
image: '/static/article-1.png',
|
||||
description: '商品分类是电商平台的重要组成部分...',
|
||||
content: '<h2>标题</h2><p>详细内容...</p>',
|
||||
status: 1,
|
||||
created_at: '2026-01-28 10:30:00',
|
||||
updated_at: '2026-01-28 10:30:00'
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文章(新建或编辑)
|
||||
* @param data 文章数据
|
||||
* @param id 文章ID(编辑时传入)
|
||||
* @returns Promise<{ success: boolean, message: string, id?: number }>
|
||||
*/
|
||||
export function saveArticle(data: ArticlePayload, id?: number): Promise<any> {
|
||||
// TODO: 替换为实际 API 调用
|
||||
// const method = id ? 'PUT' : 'POST'
|
||||
// const url = id ? `/article/${id}` : '/article'
|
||||
// return uni.$http[method === 'PUT' ? 'put' : 'post'](url, data)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: id ? '编辑成功' : '新建成功',
|
||||
id: id || Math.floor(Math.random() * 10000)
|
||||
})
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文章
|
||||
* @param id 文章ID
|
||||
* @returns Promise<{ success: boolean, message: string }>
|
||||
*/
|
||||
export function deleteArticle(id: number): Promise<any> {
|
||||
// TODO: 替换为实际 API 调用
|
||||
// return uni.$http.delete(`/article/${id}`)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布/取消发布文章
|
||||
* @param id 文章ID
|
||||
* @param status 状态 (0: 取消发布, 1: 发布)
|
||||
* @returns Promise<{ success: boolean, message: string }>
|
||||
*/
|
||||
export function publishArticle(id: number, status: number): Promise<any> {
|
||||
// TODO: 替换为实际 API 调用
|
||||
// return uni.$http.put(`/article/${id}/publish`, { status })
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: status === 1 ? '发布成功' : '取消发布成功'
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
178
mall/pages/mall/admin/article/articleCategory.uts
Normal file
178
mall/pages/mall/admin/article/articleCategory.uts
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 文章分类管理服务层
|
||||
* 提供分类列表、保存、删除等接口
|
||||
*/
|
||||
|
||||
// 分类数据结构
|
||||
export interface CategoryItem {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
image: string
|
||||
article_count: number
|
||||
sort: number
|
||||
status: number // 0: 禁用, 1: 启用
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 分类创建/编辑参数
|
||||
export interface CategoryPayload {
|
||||
name: string
|
||||
description: string
|
||||
image: string
|
||||
sort: number
|
||||
status: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类列表
|
||||
* @param params 查询参数 { page, limit, keyword, status }
|
||||
* @returns Promise<{ items: CategoryItem[], total: number }>
|
||||
*/
|
||||
export function getCategoryList(params: any = {}): Promise<any> {
|
||||
// TODO: 替换为实际 API 调用
|
||||
// return uni.$http.get('/article/category/list', { params })
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
name: '运营指南',
|
||||
description: '关于商城运营的各类指南和教程',
|
||||
image: '/static/category-1.png',
|
||||
article_count: 12,
|
||||
sort: 1,
|
||||
status: 1,
|
||||
created_at: '2026-01-15 10:00:00',
|
||||
updated_at: '2026-01-20 15:30:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '营销技巧',
|
||||
description: '营销活动策略和最佳实践',
|
||||
image: '/static/category-2.png',
|
||||
article_count: 8,
|
||||
sort: 2,
|
||||
status: 1,
|
||||
created_at: '2026-01-15 11:00:00',
|
||||
updated_at: '2026-01-19 14:20:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '常见问题',
|
||||
description: '常见问题解答和故障排除',
|
||||
image: '/static/category-3.png',
|
||||
article_count: 5,
|
||||
sort: 3,
|
||||
status: 1,
|
||||
created_at: '2026-01-15 12:00:00',
|
||||
updated_at: '2026-01-18 09:45:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '产品更新',
|
||||
description: '产品更新日志和新功能介绍',
|
||||
image: '/static/category-4.png',
|
||||
article_count: 3,
|
||||
sort: 4,
|
||||
status: 0,
|
||||
created_at: '2026-01-16 10:00:00',
|
||||
updated_at: '2026-01-17 16:00:00'
|
||||
}
|
||||
],
|
||||
total: 4
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类详情
|
||||
* @param id 分类ID
|
||||
* @returns Promise<CategoryItem>
|
||||
*/
|
||||
export function getCategoryDetail(id: number): Promise<CategoryItem> {
|
||||
// TODO: 替换为实际 API 调用
|
||||
// return uni.$http.get(`/article/category/${id}`)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
id,
|
||||
name: '运营指南',
|
||||
description: '关于商城运营的各类指南和教程',
|
||||
image: '/static/category-1.png',
|
||||
article_count: 12,
|
||||
sort: 1,
|
||||
status: 1,
|
||||
created_at: '2026-01-15 10:00:00',
|
||||
updated_at: '2026-01-20 15:30:00'
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存分类(新建或编辑)
|
||||
* @param data 分类数据
|
||||
* @param id 分类ID(编辑时传入)
|
||||
* @returns Promise<{ success: boolean, message: string, id?: number }>
|
||||
*/
|
||||
export function saveCategory(data: CategoryPayload, id?: number): Promise<any> {
|
||||
// TODO: 替换为实际 API 调用
|
||||
// const method = id ? 'PUT' : 'POST'
|
||||
// const url = id ? `/article/category/${id}` : '/article/category'
|
||||
// return uni.$http[method === 'PUT' ? 'put' : 'post'](url, data)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: id ? '编辑成功' : '新建成功',
|
||||
id: id || Math.floor(Math.random() * 10000)
|
||||
})
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分类
|
||||
* @param id 分类ID
|
||||
* @returns Promise<{ success: boolean, message: string }>
|
||||
*/
|
||||
export function deleteCategory(id: number): Promise<any> {
|
||||
// TODO: 替换为实际 API 调用
|
||||
// return uni.$http.delete(`/article/category/${id}`)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: '删除成功'
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用分类
|
||||
* @param id 分类ID
|
||||
* @param status 状态 (0: 禁用, 1: 启用)
|
||||
* @returns Promise<{ success: boolean, message: string }>
|
||||
*/
|
||||
export function toggleCategoryStatus(id: number, status: number): Promise<any> {
|
||||
// TODO: 替换为实际 API 调用
|
||||
// return uni.$http.put(`/article/category/${id}/status`, { status })
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: status === 1 ? '启用成功' : '禁用成功'
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
25
mall/pages/mall/admin/article/category.uvue
Normal file
25
mall/pages/mall/admin/article/category.uvue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面已修复 (UTF-8)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('category')
|
||||
const title = ref<string>('category')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
25
mall/pages/mall/admin/article/create.uvue
Normal file
25
mall/pages/mall/admin/article/create.uvue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面已修复 (UTF-8)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('create')
|
||||
const title = ref<string>('create')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
25
mall/pages/mall/admin/article/edit.uvue
Normal file
25
mall/pages/mall/admin/article/edit.uvue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面已修复 (UTF-8)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('edit')
|
||||
const title = ref<string>('edit')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
25
mall/pages/mall/admin/article/index.uvue
Normal file
25
mall/pages/mall/admin/article/index.uvue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面已修复 (UTF-8)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('article-index')
|
||||
const title = ref<string>('文章管理')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
81
mall/pages/mall/admin/cms/article/list.uvue
Normal file
81
mall/pages/mall/admin/cms/article/list.uvue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">文章管理</text>
|
||||
<text class="page-subtitle">Component: CmsArticle</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 文章管理 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
81
mall/pages/mall/admin/cms/category/list.uvue
Normal file
81
mall/pages/mall/admin/cms/category/list.uvue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">文章分类</text>
|
||||
<text class="page-subtitle">Component: CmsCategory</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 文章分类 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
65
mall/pages/mall/admin/content/index.uvue
Normal file
65
mall/pages/mall/admin/content/index.uvue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="content-list">
|
||||
<view class="Page">
|
||||
<view class="Header">
|
||||
<text class="Title">文章管理</text>
|
||||
<text class="SubTitle">content/index</text>
|
||||
</view>
|
||||
|
||||
<view class="Card">
|
||||
<text class="Label">页面参数(query)</text>
|
||||
<text class="Mono">{{ params }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
const params = ref('')
|
||||
|
||||
onLoad((options) => {
|
||||
// options: Record<string, any>
|
||||
params.value = JSON.stringify(options ?? {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.Page {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.Header {
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.SubTitle {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.Card {
|
||||
margin-top: 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.Mono {
|
||||
font-size: 24rpx;
|
||||
font-family: monospace;
|
||||
line-height: 36rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
98
mall/pages/mall/admin/customer-service/auto-reply.uvue
Normal file
98
mall/pages/mall/admin/customer-service/auto-reply.uvue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="cs-auto-reply">
|
||||
<view class="page">
|
||||
<view class="topbar">
|
||||
<view class="topbar-left">
|
||||
<text class="title">自动回复</text>
|
||||
<text class="subtitle">customer-service/auto-reply</text>
|
||||
</view>
|
||||
<view class="topbar-right">
|
||||
<view class="btn" @click="onBack"><text class="btn-text">返回</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<text class="h1">自动回复</text>
|
||||
<text class="p">这是页面骨架(可跑)。你可以在这里接入你们项目的 TopBar / Container 组件与业务逻辑。</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
const onBack = () => {
|
||||
// H5/小程序均可用
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f6f7fb;
|
||||
}
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #eef0f6;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
.subtitle {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
.topbar-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.topbar-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
background: #111827;
|
||||
}
|
||||
.btn-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef0f6;
|
||||
}
|
||||
.h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.p {
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
98
mall/pages/mall/admin/customer-service/config.uvue
Normal file
98
mall/pages/mall/admin/customer-service/config.uvue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="cs-config">
|
||||
<view class="page">
|
||||
<view class="topbar">
|
||||
<view class="topbar-left">
|
||||
<text class="title">客服设置</text>
|
||||
<text class="subtitle">customer-service/config</text>
|
||||
</view>
|
||||
<view class="topbar-right">
|
||||
<view class="btn" @click="onBack"><text class="btn-text">返回</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<text class="h1">客服设置</text>
|
||||
<text class="p">这是页面骨架(可跑)。你可以在这里接入你们项目的 TopBar / Container 组件与业务逻辑。</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
const onBack = () => {
|
||||
// H5/小程序均可用
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f6f7fb;
|
||||
}
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #eef0f6;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
.subtitle {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
.topbar-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.topbar-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
background: #111827;
|
||||
}
|
||||
.btn-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef0f6;
|
||||
}
|
||||
.h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.p {
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
97
mall/pages/mall/admin/customer-service/list.uvue
Normal file
97
mall/pages/mall/admin/customer-service/list.uvue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<AdminLayout current-page='list'>
|
||||
<view class="page">
|
||||
<view class="topbar">
|
||||
<view class="topbar-left">
|
||||
<text class="title">客服列表</text>
|
||||
<text class="subtitle">customer-service/list</text>
|
||||
</view>
|
||||
<view class="topbar-right">
|
||||
<view class="btn" @click="onBack"><text class="btn-text">返回</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<text class="h1">客服列表</text>
|
||||
<text class="p">这是页面骨架(可跑)。你可以在这里接入你们项目的 TopBar / Container 组件与业务逻辑。</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
const onBack = () => {
|
||||
// H5/小程序均可用
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f6f7fb;
|
||||
}
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #eef0f6;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
.subtitle {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
.topbar-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.topbar-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
background: #111827;
|
||||
}
|
||||
.btn-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef0f6;
|
||||
}
|
||||
.h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.p {
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
98
mall/pages/mall/admin/customer-service/messages.uvue
Normal file
98
mall/pages/mall/admin/customer-service/messages.uvue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="cs-message">
|
||||
<view class="page">
|
||||
<view class="topbar">
|
||||
<view class="topbar-left">
|
||||
<text class="title">客服消息</text>
|
||||
<text class="subtitle">customer-service/messages</text>
|
||||
</view>
|
||||
<view class="topbar-right">
|
||||
<view class="btn" @click="onBack"><text class="btn-text">返回</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<text class="h1">客服消息</text>
|
||||
<text class="p">这是页面骨架(可跑)。你可以在这里接入你们项目的 TopBar / Container 组件与业务逻辑。</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
const onBack = () => {
|
||||
// H5/小程序均可用
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f6f7fb;
|
||||
}
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #eef0f6;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
.subtitle {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
.topbar-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.topbar-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
background: #111827;
|
||||
}
|
||||
.btn-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef0f6;
|
||||
}
|
||||
.h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.p {
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
98
mall/pages/mall/admin/customer-service/script.uvue
Normal file
98
mall/pages/mall/admin/customer-service/script.uvue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="cs-script">
|
||||
<view class="page">
|
||||
<view class="topbar">
|
||||
<view class="topbar-left">
|
||||
<text class="title">快捷回复话术</text>
|
||||
<text class="subtitle">customer-service/script</text>
|
||||
</view>
|
||||
<view class="topbar-right">
|
||||
<view class="btn" @click="onBack"><text class="btn-text">返回</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="container">
|
||||
<view class="card">
|
||||
<text class="h1">快捷回复话术</text>
|
||||
<text class="p">这是页面骨架(可跑)。你可以在这里接入你们项目的 TopBar / Container 组件与业务逻辑。</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
const onBack = () => {
|
||||
// H5/小程序均可用
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f6f7fb;
|
||||
}
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #eef0f6;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
.subtitle {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
.topbar-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.topbar-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
background: #111827;
|
||||
}
|
||||
.btn-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef0f6;
|
||||
}
|
||||
.h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.p {
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
179
mall/pages/mall/admin/design/README.md
Normal file
179
mall/pages/mall/admin/design/README.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# 页面装修管理模块 - README
|
||||
|
||||
> 基于CRMEB项目标准,实现完整的页面装修和DIY功能
|
||||
|
||||
## 📋 文件结构
|
||||
|
||||
```
|
||||
pages/mall/admin/design/
|
||||
├── index.uvue # 装修管理主界面(898行)
|
||||
├── design.uts # 业务逻辑和数据管理(350+行)
|
||||
├── editor.uvue # 装修编辑器(待实现)
|
||||
├── preview.uvue # 装修预览页面(待实现)
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
## 🎯 核心功能
|
||||
|
||||
### 1. 首页装修 (Homepage)
|
||||
|
||||
- 自定义首页布局和显示内容
|
||||
- 支持轮播图、商品展示、文本等组件
|
||||
- 实时预览效果
|
||||
- 版本管理和发布
|
||||
|
||||
### 2. 分类页装修 (Category)
|
||||
|
||||
- 为不同商品分类创建装修页面
|
||||
- 支持多套分类装修方案
|
||||
- 按分类自动应用装修效果
|
||||
- 快速切换和对比
|
||||
|
||||
### 3. 商品页装修 (Product)
|
||||
|
||||
- 自定义商品详情页布局
|
||||
- 支持商品图、信息、评价等模块
|
||||
- 提升商品转化率
|
||||
- A/B测试支持
|
||||
|
||||
### 4. 自定义页面 (Custom)
|
||||
|
||||
- 创建和管理自定义营销页面
|
||||
- 灵活的页面路径设置
|
||||
- 独立的装修配置
|
||||
- 活动和推广专用
|
||||
|
||||
### 5. 页面模板库 (Templates)
|
||||
|
||||
- 预设4套电商风格模板
|
||||
- 一键应用模板快速建站
|
||||
- 模板库不断扩充
|
||||
- 自定义模板保存
|
||||
|
||||
### 6. 组件库 (Components)
|
||||
|
||||
- 8种预设装修组件:
|
||||
- 图片组件 (Image)
|
||||
- 文本组件 (Text)
|
||||
- 商品组件 (Product)
|
||||
- 轮播组件 (Carousel)
|
||||
- 分割线 (Divider)
|
||||
- 间距组件 (Spacer)
|
||||
- 按钮组件 (Button)
|
||||
- 表单组件 (Form)
|
||||
|
||||
## 🔧 API 函数列表
|
||||
|
||||
| 函数 | 参数 | 返回值 | 说明 |
|
||||
| -------------------------- | ---------- | -------------------------- | ---------------- |
|
||||
| `getDesignList(params?)` | 查询参数 | Promise<DesignItem[]> | 获取装修列表 |
|
||||
| `getHomePageDesign()` | 无 | Promise<DesignItem> | 获取首页装修 |
|
||||
| `getProductPageDesign()` | 无 | Promise<DesignItem> | 获取商品页装修 |
|
||||
| `getCategoryDesigns()` | 无 | Promise<DesignItem[]> | 获取分类装修列表 |
|
||||
| `getCustomPages()` | 无 | Promise<DesignItem[]> | 获取自定义页面 |
|
||||
| `getTemplateLibrary()` | 无 | Promise<DesignTemplate[]> | 获取模板库 |
|
||||
| `getAvailableComponents()` | 无 | Promise<DesignComponent[]> | 获取可用组件 |
|
||||
| `saveDesign(design)` | DesignItem | Promise<{id, message}> | 保存装修 |
|
||||
| `publishDesign(id)` | 装修ID | Promise<{message}> | 发布装修 |
|
||||
| `deleteDesign(id)` | 装修ID | Promise<{message}> | 删除装修 |
|
||||
|
||||
## 📊 数据结构
|
||||
|
||||
### DesignItem 装修页面
|
||||
|
||||
```typescript
|
||||
interface DesignItem {
|
||||
id: string | number; // 装修ID
|
||||
name: string; // 装修名称
|
||||
type: "homepage" | "category" | "product" | "custom";
|
||||
status: 0 | 1; // 0=草稿, 1=已发布
|
||||
content: DesignComponent[]; // 组件内容
|
||||
categoryId?: string | number; // 分类ID
|
||||
categoryName?: string; // 分类名称
|
||||
path?: string; // 页面路径
|
||||
version?: string; // 版本号
|
||||
created_at?: string; // 创建时间
|
||||
updated_at?: string; // 更新时间
|
||||
}
|
||||
```
|
||||
|
||||
### DesignComponent 组件配置
|
||||
|
||||
```typescript
|
||||
interface DesignComponent {
|
||||
id: string; // 组件ID
|
||||
type:
|
||||
| "image"
|
||||
| "text"
|
||||
| "product"
|
||||
| "carousel"
|
||||
| "divider"
|
||||
| "spacer"
|
||||
| "button"
|
||||
| "form";
|
||||
name: string; // 组件名称
|
||||
icon: string; // 组件图标
|
||||
description?: string; // 组件描述
|
||||
config?: Record<string, any>; // 配置参数
|
||||
children?: DesignComponent[]; // 子组件
|
||||
}
|
||||
```
|
||||
|
||||
## 💻 使用示例
|
||||
|
||||
```typescript
|
||||
// 导入服务
|
||||
import {
|
||||
getDesignList,
|
||||
saveDesign,
|
||||
publishDesign,
|
||||
getAvailableComponents,
|
||||
} from "./design.uts";
|
||||
|
||||
// 获取列表
|
||||
const designs = await getDesignList();
|
||||
|
||||
// 保存装修
|
||||
await saveDesign({
|
||||
id: 1,
|
||||
name: "首页",
|
||||
type: "homepage",
|
||||
status: 0,
|
||||
content: [],
|
||||
});
|
||||
|
||||
// 发布装修
|
||||
await publishDesign(1);
|
||||
|
||||
// 获取组件库
|
||||
const components = await getAvailableComponents();
|
||||
```
|
||||
|
||||
## 📱 菜单配置
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "design",
|
||||
"title": "设计",
|
||||
"children": [
|
||||
{
|
||||
"id": "design-home",
|
||||
"title": "页面装修",
|
||||
"path": "/pages/mall/admin/design/index"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 后续开发
|
||||
|
||||
- [ ] editor.uvue - 装修编辑器
|
||||
- [ ] preview.uvue - 装修预览
|
||||
- [ ] 拖拽排序功能
|
||||
- [ ] 版本管理
|
||||
- [ ] 模板库管理
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-01-31
|
||||
**版本**: 1.0.0
|
||||
25
mall/pages/mall/admin/design/category.uvue
Normal file
25
mall/pages/mall/admin/design/category.uvue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面已修复 (UTF-8)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('design-category')
|
||||
const title = ref<string>('分类页装修')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
25
mall/pages/mall/admin/design/components.uvue
Normal file
25
mall/pages/mall/admin/design/components.uvue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面已修复 (UTF-8)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('category')
|
||||
const title = ref<string>('category')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
439
mall/pages/mall/admin/design/config.uts
Normal file
439
mall/pages/mall/admin/design/config.uts
Normal file
@@ -0,0 +1,439 @@
|
||||
/**
|
||||
* 设计模块页面路由配置与数据
|
||||
* 将 design.uts 的函数输出转换为页面路由/配置格式
|
||||
*/
|
||||
|
||||
import type { DesignItem, DesignComponent, DesignTemplate } from './design.uts'
|
||||
|
||||
/**
|
||||
* 装修页面列表路由配置
|
||||
*/
|
||||
export const designListPageConfig = {
|
||||
id: 'design-list',
|
||||
path: '/pages/mall/admin/design/list',
|
||||
name: '装修列表',
|
||||
title: '装修管理',
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
name: '首页装修',
|
||||
type: 'homepage' as const,
|
||||
status: 1,
|
||||
path: '/pages/mall/admin/design/index?tab=homepage',
|
||||
content: [],
|
||||
updated_at: '2026-01-30 14:30:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '年货节活动页',
|
||||
type: 'custom' as const,
|
||||
status: 1,
|
||||
path: '/pages/mall/admin/design/index?tab=custom',
|
||||
content: [],
|
||||
updated_at: '2026-01-28 10:15:00'
|
||||
}
|
||||
] as DesignItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页装修页面路由配置
|
||||
*/
|
||||
export const designHomepagePageConfig = {
|
||||
id: 'design-homepage',
|
||||
path: '/pages/mall/admin/design/index?tab=homepage',
|
||||
name: '首页装修',
|
||||
title: '首页装修 - 打造吸引人的商城首页',
|
||||
data: {
|
||||
id: 'homepage',
|
||||
name: '首页装修',
|
||||
type: 'homepage' as const,
|
||||
status: 1,
|
||||
content: [
|
||||
{
|
||||
id: 'carousel-1',
|
||||
type: 'carousel' as const,
|
||||
name: '轮播图',
|
||||
icon: 'C',
|
||||
description: '首页顶部轮播图展示',
|
||||
config: {
|
||||
autoplay: true,
|
||||
duration: 5000,
|
||||
height: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'product-1',
|
||||
type: 'product' as const,
|
||||
name: '商品展示',
|
||||
icon: 'P',
|
||||
description: '热销商品列表',
|
||||
config: {
|
||||
count: 8,
|
||||
columns: 2,
|
||||
layout: 'grid'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: '1.0.0',
|
||||
updated_at: '2026-01-30 14:30:00'
|
||||
} as DesignItem
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类页装修页面路由配置
|
||||
*/
|
||||
export const designCategoryPageConfig = {
|
||||
id: 'design-category',
|
||||
path: '/pages/mall/admin/design/index?tab=category',
|
||||
name: '分类页装修',
|
||||
title: '分类页装修 - 为不同分类创建独特展示',
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
name: '默认分类装修',
|
||||
type: 'category' as const,
|
||||
status: 1,
|
||||
categoryId: 0,
|
||||
categoryName: '全部分类',
|
||||
path: '/pages/mall/admin/design/index?tab=category&id=1',
|
||||
content: [],
|
||||
updated_at: '2026-01-30 14:30:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '热销商品分类',
|
||||
type: 'category' as const,
|
||||
status: 0,
|
||||
categoryId: 1,
|
||||
categoryName: '推荐分类',
|
||||
path: '/pages/mall/admin/design/index?tab=category&id=2',
|
||||
content: [],
|
||||
updated_at: '2026-01-29 10:15:00'
|
||||
}
|
||||
] as DesignItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品页装修页面路由配置
|
||||
*/
|
||||
export const designProductPageConfig = {
|
||||
id: 'design-product',
|
||||
path: '/pages/mall/admin/design/index?tab=product',
|
||||
name: '商品页装修',
|
||||
title: '商品页装修 - 自定义商品详情页展示',
|
||||
data: {
|
||||
id: 'product',
|
||||
name: '商品页装修',
|
||||
type: 'product' as const,
|
||||
status: 1,
|
||||
content: [
|
||||
{
|
||||
id: 'image-1',
|
||||
type: 'image' as const,
|
||||
name: '商品图',
|
||||
icon: 'I',
|
||||
description: '商品主图展示'
|
||||
},
|
||||
{
|
||||
id: 'product-info',
|
||||
type: 'text' as const,
|
||||
name: '商品信息',
|
||||
icon: 'T',
|
||||
description: '商品名称和价格'
|
||||
}
|
||||
],
|
||||
version: '1.0.0',
|
||||
updated_at: '2026-01-30 14:30:00'
|
||||
} as DesignItem
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义页面路由配置
|
||||
*/
|
||||
export const designCustomPageConfig = {
|
||||
id: 'design-custom',
|
||||
path: '/pages/mall/admin/design/index?tab=custom',
|
||||
name: '自定义页面',
|
||||
title: '自定义页面 - 创建特殊内容页面',
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
name: '新年促销页',
|
||||
type: 'custom' as const,
|
||||
status: 1,
|
||||
path: '/pages/mall/admin/design/index?tab=custom&id=1',
|
||||
content: [],
|
||||
updated_at: '2026-01-28 09:00:00'
|
||||
}
|
||||
] as DesignItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板库页面路由配置
|
||||
*/
|
||||
export const designTemplatePageConfig = {
|
||||
id: 'design-templates',
|
||||
path: '/pages/mall/admin/design/index?tab=templates',
|
||||
name: '模板库',
|
||||
title: '模板库 - 选择预设装修模板',
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
name: '电商风格A',
|
||||
description: '简洁现代的电商布局',
|
||||
type: 'homepage',
|
||||
preview: '/static/images/template-a.png',
|
||||
content: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '电商风格B',
|
||||
description: '豪华展示的电商布局',
|
||||
type: 'homepage',
|
||||
preview: '/static/images/template-b.png',
|
||||
content: []
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '精品风格',
|
||||
description: '精品商品展示布局',
|
||||
type: 'homepage',
|
||||
preview: '/static/images/template-c.png',
|
||||
content: []
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '商城风格',
|
||||
description: '完整商城功能布局',
|
||||
type: 'homepage',
|
||||
preview: '/static/images/template-d.png',
|
||||
content: []
|
||||
}
|
||||
] as DesignTemplate[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件库页面路由配置
|
||||
*/
|
||||
export const designComponentPageConfig = {
|
||||
id: 'design-components',
|
||||
path: '/pages/mall/admin/design/index?tab=components',
|
||||
name: '组件库',
|
||||
title: '组件库 - 丰富的页面组件',
|
||||
data: [
|
||||
{
|
||||
id: 'image',
|
||||
type: 'image' as const,
|
||||
name: '图片组件',
|
||||
icon: 'I',
|
||||
description: '展示图片和图片轮播',
|
||||
componentName: 'ImageComponent',
|
||||
config: {
|
||||
defaultWidth: '100%',
|
||||
defaultHeight: 'auto'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
type: 'text' as const,
|
||||
name: '文本组件',
|
||||
icon: 'T',
|
||||
description: '展示文本内容和段落',
|
||||
componentName: 'TextComponent'
|
||||
},
|
||||
{
|
||||
id: 'product',
|
||||
type: 'product' as const,
|
||||
name: '商品组件',
|
||||
icon: 'P',
|
||||
description: '展示商品列表和推荐',
|
||||
componentName: 'ProductComponent',
|
||||
config: {
|
||||
defaultCount: 6,
|
||||
defaultColumns: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'carousel',
|
||||
type: 'carousel' as const,
|
||||
name: '轮播组件',
|
||||
icon: 'C',
|
||||
description: '图片和内容轮播',
|
||||
componentName: 'CarouselComponent',
|
||||
config: {
|
||||
autoplay: true,
|
||||
duration: 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'divider',
|
||||
type: 'divider' as const,
|
||||
name: '分割线',
|
||||
icon: 'D',
|
||||
description: '分割不同内容区域',
|
||||
componentName: 'DividerComponent'
|
||||
},
|
||||
{
|
||||
id: 'spacer',
|
||||
type: 'spacer' as const,
|
||||
name: '间距组件',
|
||||
icon: 'S',
|
||||
description: '调整元素间距',
|
||||
componentName: 'SpacerComponent',
|
||||
config: {
|
||||
defaultHeight: 16
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'button',
|
||||
type: 'button' as const,
|
||||
name: '按钮组件',
|
||||
icon: 'B',
|
||||
description: '创建点击按钮',
|
||||
componentName: 'ButtonComponent'
|
||||
},
|
||||
{
|
||||
id: 'form',
|
||||
type: 'form' as const,
|
||||
name: '表单组件',
|
||||
icon: 'F',
|
||||
description: '收集用户输入数据',
|
||||
componentName: 'FormComponent'
|
||||
}
|
||||
] as DesignComponent[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑页面路由配置
|
||||
*/
|
||||
export const designEditorPageConfig = {
|
||||
id: 'design-editor',
|
||||
path: '/pages/mall/admin/design/editor',
|
||||
name: '装修编辑器',
|
||||
title: '装修编辑器 - 可视化编辑装修页面',
|
||||
components: [
|
||||
{
|
||||
id: 'canvas',
|
||||
name: '编辑画布',
|
||||
description: '拖拽编辑区域'
|
||||
},
|
||||
{
|
||||
id: 'sidebar',
|
||||
name: '组件侧栏',
|
||||
description: '可用组件列表'
|
||||
},
|
||||
{
|
||||
id: 'properties',
|
||||
name: '属性面板',
|
||||
description: '组件属性编辑'
|
||||
},
|
||||
{
|
||||
id: 'preview',
|
||||
name: '预览窗口',
|
||||
description: '实时效果预览'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览页面路由配置
|
||||
*/
|
||||
export const designPreviewPageConfig = {
|
||||
id: 'design-preview',
|
||||
path: '/pages/mall/design/preview/:id',
|
||||
name: '装修预览',
|
||||
title: '装修效果预览',
|
||||
features: [
|
||||
'全屏预览',
|
||||
'响应式展示',
|
||||
'交互测试',
|
||||
'性能分析'
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有设计页面路由配置
|
||||
*/
|
||||
export const allDesignPageConfigs = [
|
||||
designListPageConfig,
|
||||
designHomepagePageConfig,
|
||||
designCategoryPageConfig,
|
||||
designProductPageConfig,
|
||||
designCustomPageConfig,
|
||||
designTemplatePageConfig,
|
||||
designComponentPageConfig,
|
||||
designEditorPageConfig,
|
||||
designPreviewPageConfig
|
||||
]
|
||||
|
||||
/**
|
||||
* 根据 tab 获取对应的页面配置
|
||||
*/
|
||||
export function getDesignPageConfig(tab: string) {
|
||||
const configMap: Record<string, any> = {
|
||||
'homepage': designHomepagePageConfig,
|
||||
'category': designCategoryPageConfig,
|
||||
'product': designProductPageConfig,
|
||||
'custom': designCustomPageConfig,
|
||||
'templates': designTemplatePageConfig,
|
||||
'components': designComponentPageConfig,
|
||||
'editor': designEditorPageConfig,
|
||||
'preview': designPreviewPageConfig,
|
||||
'list': designListPageConfig
|
||||
}
|
||||
return configMap[tab] || designListPageConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* 装修页面导航菜单结构
|
||||
*/
|
||||
export const designMenuStructure = {
|
||||
id: 'design',
|
||||
title: '设计',
|
||||
icon: '/static/design.svg',
|
||||
path: '/pages/mall/admin/design/index',
|
||||
children: [
|
||||
{
|
||||
id: 'page-decoration',
|
||||
title: '页面装修',
|
||||
children: [
|
||||
{
|
||||
id: 'design-homepage',
|
||||
title: '首页装修',
|
||||
path: '/pages/mall/admin/design/index?tab=homepage'
|
||||
},
|
||||
{
|
||||
id: 'design-category',
|
||||
title: '分类页装修',
|
||||
path: '/pages/mall/admin/design/index?tab=category'
|
||||
},
|
||||
{
|
||||
id: 'design-product',
|
||||
title: '商品页装修',
|
||||
path: '/pages/mall/admin/design/index?tab=product'
|
||||
},
|
||||
{
|
||||
id: 'design-custom',
|
||||
title: '自定义页面',
|
||||
path: '/pages/mall/admin/design/index?tab=custom'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'design-library',
|
||||
title: '设计库',
|
||||
children: [
|
||||
{
|
||||
id: 'design-templates',
|
||||
title: '模板库',
|
||||
path: '/pages/mall/admin/design/index?tab=templates'
|
||||
},
|
||||
{
|
||||
id: 'design-components',
|
||||
title: '组件库',
|
||||
path: '/pages/mall/admin/design/index?tab=components'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
25
mall/pages/mall/admin/design/custom.uvue
Normal file
25
mall/pages/mall/admin/design/custom.uvue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面已修复 (UTF-8)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('custom')
|
||||
const title = ref<string>('custom')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
38
mall/pages/mall/admin/design/data-config.uvue
Normal file
38
mall/pages/mall/admin/design/data-config.uvue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<AdminLayout current-page="design-data">
|
||||
<view class="admin-main">
|
||||
<view class="header">
|
||||
<text class="title">数据配置</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text>商城数据配置(建设中)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-main {
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.content {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
549
mall/pages/mall/admin/design/design.uts
Normal file
549
mall/pages/mall/admin/design/design.uts
Normal file
@@ -0,0 +1,549 @@
|
||||
/**
|
||||
* 页面装修业务逻辑模块
|
||||
* 参考CRMEB项目,提供完整的装修管理功能
|
||||
*/
|
||||
|
||||
/**
|
||||
* 装修页面数据接口
|
||||
*/
|
||||
export interface DesignItem {
|
||||
id: string | number
|
||||
name: string
|
||||
type: 'homepage' | 'category' | 'product' | 'custom'
|
||||
status: 0 | 1 // 0: 草稿, 1: 已发布
|
||||
categoryId?: string | number
|
||||
categoryName?: string
|
||||
path?: string
|
||||
preview_url?: string
|
||||
content: DesignComponent[]
|
||||
version?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 装修组件接口
|
||||
*/
|
||||
export interface DesignComponent {
|
||||
id: string
|
||||
type: 'image' | 'text' | 'product' | 'carousel' | 'divider' | 'spacer' | 'button' | 'form'
|
||||
name: string
|
||||
icon: string
|
||||
description?: string
|
||||
componentName?: string
|
||||
config?: Record<string, any>
|
||||
children?: DesignComponent[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 装修模板接口
|
||||
*/
|
||||
export interface DesignTemplate {
|
||||
id: string | number
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
preview: string
|
||||
content: DesignComponent[]
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取装修页面列表
|
||||
* @param params 查询参数
|
||||
* @returns 装修页面列表
|
||||
*/
|
||||
export function getDesignList(params?: Record<string, any>): Promise<DesignItem[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// TODO: 实际应调用后端API
|
||||
const designList: DesignItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '首页装修',
|
||||
type: 'homepage',
|
||||
status: 1,
|
||||
content: [],
|
||||
updated_at: '2026-01-30 14:30:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '年货节活动页',
|
||||
type: 'custom',
|
||||
status: 1,
|
||||
content: [],
|
||||
updated_at: '2026-01-28 10:15:00'
|
||||
}
|
||||
]
|
||||
setTimeout(() => resolve(designList), 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取首页装修详情
|
||||
* @returns 首页装修数据
|
||||
*/
|
||||
export function getHomePageDesign(): Promise<DesignItem> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const homepage: DesignItem = {
|
||||
id: 'homepage',
|
||||
name: '首页装修',
|
||||
type: 'homepage',
|
||||
status: 1,
|
||||
content: [
|
||||
{
|
||||
id: 'carousel-1',
|
||||
type: 'carousel',
|
||||
name: '轮播图',
|
||||
icon: 'C',
|
||||
description: '首页顶部轮播图展示',
|
||||
config: {
|
||||
autoplay: true,
|
||||
duration: 5000,
|
||||
height: 300
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'product-1',
|
||||
type: 'product',
|
||||
name: '商品展示',
|
||||
icon: 'P',
|
||||
description: '热销商品列表',
|
||||
config: {
|
||||
count: 8,
|
||||
columns: 2,
|
||||
layout: 'grid'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: '1.0.0',
|
||||
updated_at: '2026-01-30 14:30:00'
|
||||
}
|
||||
setTimeout(() => resolve(homepage), 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品页装修详情
|
||||
* @returns 商品页装修数据
|
||||
*/
|
||||
export function getProductPageDesign(): Promise<DesignItem> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const productPage: DesignItem = {
|
||||
id: 'product',
|
||||
name: '商品页装修',
|
||||
type: 'product',
|
||||
status: 1,
|
||||
content: [
|
||||
{
|
||||
id: 'image-1',
|
||||
type: 'image',
|
||||
name: '商品图',
|
||||
icon: 'I',
|
||||
description: '商品主图展示'
|
||||
},
|
||||
{
|
||||
id: 'product-info',
|
||||
type: 'text',
|
||||
name: '商品信息',
|
||||
icon: 'T',
|
||||
description: '商品名称和价格'
|
||||
}
|
||||
],
|
||||
version: '1.0.0',
|
||||
updated_at: '2026-01-30 14:30:00'
|
||||
}
|
||||
setTimeout(() => resolve(productPage), 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类装修列表
|
||||
* @returns 分类装修列表
|
||||
*/
|
||||
export function getCategoryDesigns(): Promise<DesignItem[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const categories: DesignItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '默认分类装修',
|
||||
type: 'category',
|
||||
status: 1,
|
||||
categoryId: 0,
|
||||
categoryName: '全部分类',
|
||||
content: [],
|
||||
updated_at: '2026-01-30 14:30:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '热销商品分类',
|
||||
type: 'category',
|
||||
status: 0,
|
||||
categoryId: 1,
|
||||
categoryName: '推荐分类',
|
||||
content: [],
|
||||
updated_at: '2026-01-29 10:15:00'
|
||||
}
|
||||
]
|
||||
setTimeout(() => resolve(categories), 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义页面列表
|
||||
* @returns 自定义页面列表
|
||||
*/
|
||||
export function getCustomPages(): Promise<DesignItem[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const customPages: DesignItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '新年促销页',
|
||||
type: 'custom',
|
||||
status: 1,
|
||||
path: '/pages/promotion/newyear',
|
||||
content: [],
|
||||
updated_at: '2026-01-28 09:00:00'
|
||||
}
|
||||
]
|
||||
setTimeout(() => resolve(customPages), 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取页面模板库
|
||||
* @returns 模板列表
|
||||
*/
|
||||
export function getTemplateLibrary(): Promise<DesignTemplate[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const templates: DesignTemplate[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: '电商风格A',
|
||||
description: '简洁现代的电商布局',
|
||||
type: 'homepage',
|
||||
preview: '@/static/images/template-a.png',
|
||||
content: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '电商风格B',
|
||||
description: '豪华展示的电商布局',
|
||||
type: 'homepage',
|
||||
preview: '@/static/images/template-b.png',
|
||||
content: []
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '精品风格',
|
||||
description: '精品商品展示布局',
|
||||
type: 'homepage',
|
||||
preview: '@/static/images/template-c.png',
|
||||
content: []
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '商城风格',
|
||||
description: '完整商城功能布局',
|
||||
type: 'homepage',
|
||||
preview: '@/static/images/template-d.png',
|
||||
content: []
|
||||
}
|
||||
]
|
||||
setTimeout(() => resolve(templates), 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用组件库
|
||||
* @returns 组件列表
|
||||
*/
|
||||
export function getAvailableComponents(): Promise<DesignComponent[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const components: DesignComponent[] = [
|
||||
{
|
||||
id: 'image',
|
||||
type: 'image',
|
||||
name: '图片组件',
|
||||
icon: 'I',
|
||||
description: '展示图片和图片轮播',
|
||||
componentName: 'ImageComponent',
|
||||
config: {
|
||||
defaultWidth: '100%',
|
||||
defaultHeight: 'auto'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
type: 'text',
|
||||
name: '文本组件',
|
||||
icon: 'T',
|
||||
description: '展示文本内容和段落',
|
||||
componentName: 'TextComponent'
|
||||
},
|
||||
{
|
||||
id: 'product',
|
||||
type: 'product',
|
||||
name: '商品组件',
|
||||
icon: 'P',
|
||||
description: '展示商品列表和推荐',
|
||||
componentName: 'ProductComponent',
|
||||
config: {
|
||||
defaultCount: 6,
|
||||
defaultColumns: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'carousel',
|
||||
type: 'carousel',
|
||||
name: '轮播组件',
|
||||
icon: 'C',
|
||||
description: '图片和内容轮播',
|
||||
componentName: 'CarouselComponent',
|
||||
config: {
|
||||
autoplay: true,
|
||||
duration: 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'divider',
|
||||
type: 'divider',
|
||||
name: '分割线',
|
||||
icon: 'D',
|
||||
description: '分割不同内容区域',
|
||||
componentName: 'DividerComponent'
|
||||
},
|
||||
{
|
||||
id: 'spacer',
|
||||
type: 'spacer',
|
||||
name: '间距组件',
|
||||
icon: 'S',
|
||||
description: '调整元素间距',
|
||||
componentName: 'SpacerComponent',
|
||||
config: {
|
||||
defaultHeight: 16
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'button',
|
||||
type: 'button',
|
||||
name: '按钮组件',
|
||||
icon: 'B',
|
||||
description: '创建点击按钮',
|
||||
componentName: 'ButtonComponent'
|
||||
},
|
||||
{
|
||||
id: 'form',
|
||||
type: 'form',
|
||||
name: '表单组件',
|
||||
icon: 'F',
|
||||
description: '收集用户输入数据',
|
||||
componentName: 'FormComponent'
|
||||
}
|
||||
]
|
||||
setTimeout(() => resolve(components), 300)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存装修页面
|
||||
* @param design 装修数据
|
||||
* @returns 保存结果
|
||||
*/
|
||||
export function saveDesign(design: DesignItem): Promise<{ id: string | number; message: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!design.name || design.name.trim() === '') {
|
||||
reject(new Error('装修名称不能为空'))
|
||||
return
|
||||
}
|
||||
if (!design.type) {
|
||||
reject(new Error('装修类型不能为空'))
|
||||
return
|
||||
}
|
||||
// TODO: 实际应调用后端API保存
|
||||
const result = {
|
||||
id: design.id || Math.random().toString(36).substr(2, 9),
|
||||
message: '保存成功'
|
||||
}
|
||||
setTimeout(() => resolve(result), 500)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布装修页面
|
||||
* @param designId 装修页面ID
|
||||
* @returns 发布结果
|
||||
*/
|
||||
export function publishDesign(designId: string | number): Promise<{ message: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!designId) {
|
||||
reject(new Error('装修ID不能为空'))
|
||||
return
|
||||
}
|
||||
// TODO: 实际应调用后端API发布
|
||||
setTimeout(() => {
|
||||
resolve({ message: '发布成功' })
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除装修页面
|
||||
* @param designId 装修页面ID
|
||||
* @returns 删除结果
|
||||
*/
|
||||
export function deleteDesign(designId: string | number): Promise<{ message: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!designId) {
|
||||
reject(new Error('装修ID不能为空'))
|
||||
return
|
||||
}
|
||||
// TODO: 实际应调用后端API删除
|
||||
setTimeout(() => {
|
||||
resolve({ message: '删除成功' })
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取装修预览URL
|
||||
* @param designId 装修ID
|
||||
* @returns 预览URL
|
||||
*/
|
||||
export function getDesignPreviewUrl(designId: string | number): string {
|
||||
return `/pages/mall/design/preview/${designId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取装修编辑URL
|
||||
* @param designId 装修ID
|
||||
* @returns 编辑URL
|
||||
*/
|
||||
export function getDesignEditorUrl(designId: string | number): string {
|
||||
return `/pages/mall/admin/design/editor?id=${designId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
* @param dateStr 日期字符串
|
||||
* @returns 格式化后的日期
|
||||
*/
|
||||
export function formatDateTime(dateStr?: string): string {
|
||||
if (!dateStr) return '--'
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
} catch {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证组件配置
|
||||
* @param component 组件配置
|
||||
* @returns 验证结果
|
||||
*/
|
||||
export function validateComponent(component: DesignComponent): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!component.id) {
|
||||
errors.push('组件ID不能为空')
|
||||
}
|
||||
|
||||
if (!component.type) {
|
||||
errors.push('组件类型不能为空')
|
||||
}
|
||||
|
||||
if (!component.name) {
|
||||
errors.push('组件名称不能为空')
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成组件ID
|
||||
* @param type 组件类型
|
||||
* @returns 生成的组件ID
|
||||
*/
|
||||
export function generateComponentId(type: string): string {
|
||||
const timestamp = Date.now().toString(36)
|
||||
const random = Math.random().toString(36).substr(2, 5)
|
||||
return `${type}-${timestamp}-${random}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取装修约束条件
|
||||
* @returns 约束条件对象
|
||||
*/
|
||||
export function getDesignConstraints(): DesignConstraints {
|
||||
return {
|
||||
maxComponents: 50,
|
||||
allowedComponentTypes: ['image', 'text', 'product', 'carousel', 'divider', 'spacer', 'button', 'form'],
|
||||
maxImageSize: 5242880, // 5MB
|
||||
supportedImageFormats: ['jpg', 'jpeg', 'png', 'gif', 'webp']
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度克隆装修数据
|
||||
* @param design 装修数据
|
||||
* @returns 克隆后的数据
|
||||
*/
|
||||
export function cloneDesign(design: DesignItem): DesignItem {
|
||||
return JSON.parse(JSON.stringify(design))
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证装修数据完整性
|
||||
* @param design 装修数据
|
||||
* @returns 验证结果
|
||||
*/
|
||||
export function validateDesign(design: DesignItem): { valid: boolean; message: string } {
|
||||
if (!design.name || design.name.trim() === '') {
|
||||
return { valid: false, message: '装修名称不能为空' }
|
||||
}
|
||||
|
||||
if (!design.type) {
|
||||
return { valid: false, message: '装修类型不能为空' }
|
||||
}
|
||||
|
||||
if (!Array.isArray(design.content)) {
|
||||
return { valid: false, message: '装修内容格式错误' }
|
||||
}
|
||||
|
||||
if (design.content.length > getDesignConstraints().maxComponents) {
|
||||
return { valid: false, message: `组件数量超过限制(最多${getDesignConstraints().maxComponents}个)` }
|
||||
}
|
||||
|
||||
return { valid: true, message: '验证通过' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出装修为JSON
|
||||
* @param design 装修数据
|
||||
* @returns JSON字符串
|
||||
*/
|
||||
export function exportDesignJSON(design: DesignItem): string {
|
||||
return JSON.stringify(design, null, 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JSON导入装修
|
||||
* @param jsonStr JSON字符串
|
||||
* @returns 装修数据
|
||||
*/
|
||||
export function importDesignJSON(jsonStr: string): DesignItem {
|
||||
try {
|
||||
return JSON.parse(jsonStr) as DesignItem
|
||||
} catch (error) {
|
||||
throw new Error('JSON格式错误,无法导入')
|
||||
}
|
||||
}
|
||||
25
mall/pages/mall/admin/design/homepage.uvue
Normal file
25
mall/pages/mall/admin/design/homepage.uvue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面已修复 (UTF-8)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('design-homepage')
|
||||
const title = ref<string>('首页装修')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
65
mall/pages/mall/admin/design/index.uvue
Normal file
65
mall/pages/mall/admin/design/index.uvue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="design-home">
|
||||
<view class="Page">
|
||||
<view class="Header">
|
||||
<text class="Title">页面装修</text>
|
||||
<text class="SubTitle">design/index</text>
|
||||
</view>
|
||||
|
||||
<view class="Card">
|
||||
<text class="Label">页面参数(query)</text>
|
||||
<text class="Mono">{{ params }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
const params = ref('')
|
||||
|
||||
onLoad((options) => {
|
||||
// options: Record<string, any>
|
||||
params.value = JSON.stringify(options ?? {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.Page {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.Header {
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.SubTitle {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.Card {
|
||||
margin-top: 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.Mono {
|
||||
font-size: 24rpx;
|
||||
font-family: monospace;
|
||||
line-height: 36rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
38
mall/pages/mall/admin/design/link-management.uvue
Normal file
38
mall/pages/mall/admin/design/link-management.uvue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<AdminLayout current-page="design-link">
|
||||
<view class="admin-main">
|
||||
<view class="header">
|
||||
<text class="title">链接管理</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text>商城链接管理(建设中)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-main {
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.content {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
38
mall/pages/mall/admin/design/material.uvue
Normal file
38
mall/pages/mall/admin/design/material.uvue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<AdminLayout current-page="design-material">
|
||||
<view class="admin-main">
|
||||
<view class="header">
|
||||
<text class="title">素材管理</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text>商城素材管理(建设中)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-main {
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.content {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
25
mall/pages/mall/admin/design/product.uvue
Normal file
25
mall/pages/mall/admin/design/product.uvue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面已修复 (UTF-8)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('product')
|
||||
const title = ref<string>('product')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
162
mall/pages/mall/admin/design/templates.uvue
Normal file
162
mall/pages/mall/admin/design/templates.uvue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="design-container">
|
||||
<view class="module-header">
|
||||
<text class="module-title">模板库</text>
|
||||
<text class="module-desc">从丰富的模板中快速创建页面</text>
|
||||
</view>
|
||||
<view class="templates-grid">
|
||||
<view v-for="template in templateLibrary" :key="template.id" class="template-card">
|
||||
<view class="template-header">
|
||||
<text class="template-name">{{ template.name }}</text>
|
||||
</view>
|
||||
<view class="template-body">
|
||||
<text class="template-desc">{{ template.description }}</text>
|
||||
<button class="btn-use" @click="handleUseTemplate(template.id)">使用模板</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
const currentPage = ref<string>('design-templates')
|
||||
|
||||
const templateLibrary = ref<any[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '首页模板A',
|
||||
description: '经典电商首页布局'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '首页模板B',
|
||||
description: '简约风格的商城页面'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '活动模板',
|
||||
description: '活动促销页面布局'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '商品模板',
|
||||
description: '商品展示页面布局'
|
||||
}
|
||||
])
|
||||
|
||||
const handleUseTemplate = (templateId: number) => {
|
||||
console.log('使用模板', templateId)
|
||||
uni.showToast({
|
||||
title: '使用模板成功',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
|
||||
.design-container {
|
||||
min-height: 100vh;
|
||||
background: $background-secondary;
|
||||
padding: $space-lg;
|
||||
}
|
||||
|
||||
.module-header {
|
||||
margin-bottom: $space-xl;
|
||||
}
|
||||
|
||||
.module-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: bold;
|
||||
color: $text-primary;
|
||||
display: block;
|
||||
margin-bottom: $space-sm;
|
||||
}
|
||||
|
||||
.module-desc {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.templates-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: $space-lg;
|
||||
margin-bottom: $space-lg;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
background: white;
|
||||
border-radius: $radius-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: $shadow-md;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.template-header {
|
||||
padding: $space-lg;
|
||||
background: $background-tertiary;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.template-body {
|
||||
padding: $space-lg;
|
||||
}
|
||||
|
||||
.template-name {
|
||||
font-size: $font-size-md;
|
||||
font-weight: bold;
|
||||
color: $text-primary;
|
||||
display: block;
|
||||
margin-bottom: $space-sm;
|
||||
}
|
||||
|
||||
.template-desc {
|
||||
color: $text-secondary;
|
||||
font-size: $font-size-sm;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
margin-bottom: $space-lg;
|
||||
}
|
||||
|
||||
.btn-use {
|
||||
background: $primary-color;
|
||||
color: white;
|
||||
padding: $space-sm $space-lg;
|
||||
border-radius: $radius-sm;
|
||||
border: none;
|
||||
margin-top: $space-lg;
|
||||
font-size: $font-size-sm;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.design-container {
|
||||
padding: $space-md;
|
||||
}
|
||||
|
||||
.templates-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.templates-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
38
mall/pages/mall/admin/design/theme-style.uvue
Normal file
38
mall/pages/mall/admin/design/theme-style.uvue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<AdminLayout current-page="design-theme">
|
||||
<view class="admin-main">
|
||||
<view class="header">
|
||||
<text class="title">主题风格</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text>商城主题风格设置(建设中)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-main {
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.content {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
38
mall/pages/mall/admin/design/user.uvue
Normal file
38
mall/pages/mall/admin/design/user.uvue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<AdminLayout current-page="design-user">
|
||||
<view class="admin-main">
|
||||
<view class="header">
|
||||
<text class="title">个人中心装修</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text>个人中心页面装修(建设中)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-main {
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.content {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
81
mall/pages/mall/admin/finance/record.uvue
Normal file
81
mall/pages/mall/admin/finance/record.uvue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">财务记录</text>
|
||||
<text class="page-subtitle">Component: FinanceRecord</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 财务记录 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
187
mall/pages/mall/admin/homePage/components/KpiMiniCard.uvue
Normal file
187
mall/pages/mall/admin/homePage/components/KpiMiniCard.uvue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<view class="kpi-card">
|
||||
<!-- Header -->
|
||||
<view class="kpi-header">
|
||||
<text class="kpi-title">{{ title }}</text>
|
||||
|
||||
<view v-if="tagText" class="kpi-tag">
|
||||
<text class="kpi-tag-text">{{ tagText }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 可选:你想在右上角塞额外按钮/图标 -->
|
||||
<slot name="headerRight"></slot>
|
||||
</view>
|
||||
|
||||
<!-- Body -->
|
||||
<view class="kpi-body">
|
||||
<text class="kpi-main-value">{{ valuePrefix }}{{ valueText }}</text>
|
||||
|
||||
<!-- 中间“昨日 / 日环比”行(可完全替换) -->
|
||||
<view v-if="metaLeft || metaRight" class="kpi-meta">
|
||||
<text v-if="metaLeft" class="kpi-meta-text">{{ metaLeft }}</text>
|
||||
|
||||
<view v-if="metaRight" class="kpi-meta-right">
|
||||
<text class="kpi-meta-text">{{ metaRight }}</text>
|
||||
|
||||
<text
|
||||
v-if="trend !== 'none'"
|
||||
class="kpi-trend-arrow"
|
||||
:class="trendClass"
|
||||
>
|
||||
{{ trendArrow }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 可选:完全自定义这行 -->
|
||||
<slot name="meta"></slot>
|
||||
</view>
|
||||
|
||||
<view class="kpi-divider"></view>
|
||||
|
||||
<!-- 底部一行:左文案 + 右数值 -->
|
||||
<view class="kpi-footer">
|
||||
<text class="kpi-footer-left">{{ footerLeftText }}</text>
|
||||
<text class="kpi-footer-right">{{ footerRightText }}</text>
|
||||
|
||||
<!-- 可选:完全自定义 footer -->
|
||||
<slot name="footer"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
// Header
|
||||
title: string
|
||||
tagText?: string
|
||||
|
||||
// Body main
|
||||
valueText: string
|
||||
valuePrefix?: string // 例如 "¥"
|
||||
|
||||
// Meta line (可替换)
|
||||
metaLeft?: string // 例如 "昨日 4"
|
||||
metaRight?: string // 例如 "日环比 0%"
|
||||
trend?: 'up' | 'down' | 'flat' | 'none' // none = 不显示箭头
|
||||
|
||||
// Footer
|
||||
footerLeftText: string // 例如 "本月订单量"
|
||||
footerRightText: string // 例如 "181单"
|
||||
}>(), {
|
||||
tagText: '今日',
|
||||
valuePrefix: '',
|
||||
metaLeft: '',
|
||||
metaRight: '',
|
||||
trend: 'none'
|
||||
})
|
||||
|
||||
const trendArrow = computed((): string => {
|
||||
if (props.trend === 'up') return '▲'
|
||||
if (props.trend === 'down') return '▼'
|
||||
return '•'
|
||||
})
|
||||
|
||||
const trendClass = computed((): string => {
|
||||
if (props.trend === 'up') return 'is-up'
|
||||
if (props.trend === 'down') return 'is-down'
|
||||
return 'is-flat'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.kpi-card{
|
||||
background-color:#ffffff;
|
||||
border:1px solid #ebeef5;
|
||||
border-radius:6px;
|
||||
padding:16px;
|
||||
box-shadow:0 2px 12px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.kpi-header{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
|
||||
.kpi-title{
|
||||
font-size:14px;
|
||||
color:#303133;
|
||||
font-weight:600;
|
||||
}
|
||||
.kpi-tag{
|
||||
padding:2px 8px;
|
||||
border-radius:4px;
|
||||
border:1px solid #e1f3d8;
|
||||
background:#f0f9eb;
|
||||
}
|
||||
.kpi-tag-text{
|
||||
font-size:12px;
|
||||
color:#67c23a;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Body */
|
||||
.kpi-body{
|
||||
margin-top:10px;
|
||||
.kpi-main-value{
|
||||
font-size:32px;
|
||||
font-weight:600;
|
||||
color:#303133;
|
||||
line-height:40px;
|
||||
}
|
||||
|
||||
/* “昨日 / 日环比” */
|
||||
.kpi-meta{
|
||||
margin-top:8px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:flex-start;
|
||||
gap:12px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.kpi-meta-text{
|
||||
font-size:12px;
|
||||
color:#909399;
|
||||
}
|
||||
.kpi-meta-right{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:6px;
|
||||
}
|
||||
.kpi-trend-arrow{
|
||||
font-size:12px;
|
||||
}
|
||||
.kpi-trend-arrow.is-up{ color:#f56c6c; }
|
||||
.kpi-trend-arrow.is-down{ color:#67c23a; }
|
||||
.kpi-trend-arrow.is-flat{ color:#909399; }
|
||||
|
||||
.kpi-divider{
|
||||
height:1px;
|
||||
background:#ebeef5;
|
||||
margin:12px 0;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.kpi-footer{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
}
|
||||
.kpi-footer-left{
|
||||
font-size:12px;
|
||||
color:#909399;
|
||||
}
|
||||
.kpi-footer-right{
|
||||
font-size:12px;
|
||||
color:#909399;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
24
mall/pages/mall/admin/homePage/index.uvue
Normal file
24
mall/pages/mall/admin/homePage/index.uvue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<!-- 管理后台入口:直接加载 AdminLayout,使用 CRMEB 内部路由系统 -->
|
||||
<AdminLayout />
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
/**
|
||||
* 管理后台入口页面
|
||||
*
|
||||
* 架构说明:
|
||||
* 1. 此页面是 pages.json 中配置的主入口
|
||||
* 2. 直接加载 AdminLayout 组件作为容器
|
||||
* 3. AdminLayout 内部使用 CRMEB 路由系统管理所有子页面
|
||||
* 4. 不需要额外的业务逻辑,保持简洁
|
||||
*
|
||||
* 路由流程:
|
||||
* pages.json → homePage/index.uvue → AdminLayout → 内部路由切换
|
||||
*/
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 无需额外样式,完全由 AdminLayout 控制布局 */
|
||||
</style>
|
||||
483
mall/pages/mall/admin/homePage/index.uvue.bak
Normal file
483
mall/pages/mall/admin/homePage/index.uvue.bak
Normal file
@@ -0,0 +1,483 @@
|
||||
<template>
|
||||
<!-- 直接加载 AdminLayout,使用 CRMEB 内部路由系统 -->
|
||||
<AdminLayout />
|
||||
|
||||
|
||||
<!-- 第二行:订单统计图表 -->
|
||||
<view class="chart-section">
|
||||
<view class="admin-card">
|
||||
<view class="admin-card-header">
|
||||
<view class="header-left">
|
||||
<view class="title-icon">
|
||||
<!-- 不用 emoji,纯样式画一个“图表感”的小方块 -->
|
||||
<view class="title-icon-mark"></view>
|
||||
</view>
|
||||
<text class="admin-card-title">订单</text>
|
||||
</view>
|
||||
|
||||
<view class="chart-controls">
|
||||
<view
|
||||
v-for="p in chartPeriods"
|
||||
:key="p.value"
|
||||
class="seg-btn"
|
||||
:class="{ active: selectedPeriod === p.value }"
|
||||
@click="changePeriod(p.value)"
|
||||
>
|
||||
<text class="seg-btn-text">{{ p.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="admin-card-body">
|
||||
<!-- 图表容器:你后面接 ECharts / uCharts 都挂这里 -->
|
||||
<view class="echarts-container">
|
||||
<!-- 先空着也行;不要放 emoji 占位符 -->
|
||||
111
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第三行:用户统计图表 -->
|
||||
<view class="charts-row">
|
||||
<!-- 用户趋势折线图 -->
|
||||
<view class="chart-col">
|
||||
<view class="admin-card">
|
||||
<view class="admin-card-header">
|
||||
<text class="admin-card-title">用户趋势</text>
|
||||
</view>
|
||||
<view class="admin-card-body">
|
||||
<view class="echarts-container">
|
||||
<text class="chart-placeholder">📈 ECharts 折线图:用户增长趋势</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户构成饼图 -->
|
||||
<view class="chart-col">
|
||||
<view class="admin-card">
|
||||
<view class="admin-card-header">
|
||||
<text class="admin-card-title">用户构成</text>
|
||||
</view>
|
||||
<view class="admin-card-body">
|
||||
<view class="echarts-container">
|
||||
<text class="chart-placeholder">🥧 ECharts 饼图:用户来源分布</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import KpiMiniCard from './components/KpiMiniCard.uvue'
|
||||
|
||||
// KPI 数据
|
||||
const kpiData = ref({
|
||||
sales: {
|
||||
today: 125680.50,
|
||||
yesterday: 118920.30,
|
||||
monthTotal: 2857808.90,
|
||||
change: 5.7
|
||||
},
|
||||
visits: {
|
||||
today: 15420,
|
||||
yesterday: 14890,
|
||||
monthTotal: 342680,
|
||||
change: 3.4
|
||||
},
|
||||
orders: {
|
||||
today: 342,
|
||||
yesterday: 318,
|
||||
monthTotal: 8956,
|
||||
change: 7.5
|
||||
},
|
||||
users: {
|
||||
today: 156,
|
||||
yesterday: 142,
|
||||
monthTotal: 3245,
|
||||
change: 9.9
|
||||
}
|
||||
})
|
||||
|
||||
// 图表配置
|
||||
const selectedPeriod = ref('30days')
|
||||
const selectedPeriodLabel = computed((): string => {
|
||||
const hit = chartPeriods.value.find((x) => x.value === selectedPeriod.value)
|
||||
return hit ? hit.label : ""
|
||||
})
|
||||
|
||||
const chartPeriods = [
|
||||
{ label: '30天', value: '30days' },
|
||||
{ label: '周', value: 'week' },
|
||||
{ label: '月', value: 'month' },
|
||||
{ label: '年', value: 'year' }
|
||||
]
|
||||
|
||||
type PeriodItem = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
|
||||
// 方法
|
||||
const formatNumber = (num: number) => {
|
||||
if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + '万'
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'k'
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const changePeriod = (period: string) => {
|
||||
selectedPeriod.value = period
|
||||
const periodMap: Record<string, string> = {
|
||||
'30days': '30天',
|
||||
'week': '周',
|
||||
'month': '月',
|
||||
'year': '年'
|
||||
}
|
||||
selectedPeriodLabel.value = periodMap[period] || '30天'
|
||||
|
||||
// TODO: 重新加载图表数据
|
||||
console.log('切换时间粒度:', period)
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* ===== Dashboard 页面样式 ===== */
|
||||
.dashboard-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ===== KPI 卡片行 ===== */
|
||||
/* 第一行:4 个 KPI 卡片一行 */
|
||||
.kpi-cards-row{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)); /* 一行 4 列等分 */
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* 卡片本体:不要写死宽高 */
|
||||
.kpi-card{
|
||||
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
height: 200px; /* 你可以改成 140/160,别写死 200px */
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20rpx;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
|
||||
/* 响应式:宽度不够时变 2 列 / 1 列(可选) */
|
||||
@media (max-width: 1200px){
|
||||
.kpi-cards-row{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 768px){
|
||||
.kpi-cards-row{ grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
|
||||
.kpi-card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.kpi-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.kpi-card-title {
|
||||
position: absolute;
|
||||
|
||||
top: 10rpx;
|
||||
left: 5rpx;
|
||||
font-size: 16px;
|
||||
color: #666666;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.kpi-card-tag {
|
||||
background-color: #1890ff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.kpi-tag-text {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.kpi-card-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kpi-value-number {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.kpi-value-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
border-radius: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.kpi-value-trend.up {
|
||||
background-color: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.kpi-value-trend.down {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.kpi-trend-text {
|
||||
margin-left: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.kpi-card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.kpi-footer-text {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.kpi-card-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== 图表区域 ===== */
|
||||
|
||||
|
||||
/* 卡片外观 */
|
||||
.admin-card {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 头部:左标题 + 右分段按钮(不换行) */
|
||||
.admin-card-header {
|
||||
padding: 16px 24px 12px 24px;
|
||||
display: flex;
|
||||
flex-direction: row;;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap; /* 防止被挤下去 */
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: row;;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 14px;
|
||||
background: #e6f4ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title-icon-mark {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 4px;
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
.admin-card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 分段控件:一整条外框 + 内部分段(完全贴近你第二张图右上角) */
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
flex-direction: row;;
|
||||
align-items: center;
|
||||
justify-content: center;;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
flex-shrink: 0; /* 防止被压缩换行 */
|
||||
}
|
||||
|
||||
.seg-btn {
|
||||
height: 32px;
|
||||
min-width: 44px;
|
||||
padding: 0 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-left: 1px solid #d9d9d9;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.seg-btn:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.seg-btn-text {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.seg-btn.active {
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
.seg-btn.active .seg-btn-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ✅ 注意:body 是 header 的兄弟,不要写进 header 嵌套里 */
|
||||
.admin-card-body {
|
||||
padding: 0 24px 16px 24px;
|
||||
}
|
||||
|
||||
.echarts-container {
|
||||
width: 100%;
|
||||
height: 300px; /* 贴近截图比例 */
|
||||
}
|
||||
|
||||
.charts-row{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
/* 每个图表列容器 */
|
||||
.chart-col{
|
||||
min-width: 0; /* 防止 ECharts/SVG 内容把列撑爆 */
|
||||
}
|
||||
|
||||
/* ===== 响应式设计 ===== */
|
||||
@media (max-width: 1200px) {
|
||||
|
||||
.kpi-card {
|
||||
min-width: 45%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
||||
.charts-row{
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.kpi-cards-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.dashboard-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.kpi-cards-row,
|
||||
.chart-section,
|
||||
.charts-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.admin-card-header,
|
||||
.admin-card-body {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 图标字体 ===== */
|
||||
.iconfont {
|
||||
font-family: 'iconfont';
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-up:before {
|
||||
content: '↑';
|
||||
}
|
||||
|
||||
.icon-down:before {
|
||||
content: '↓';
|
||||
}
|
||||
|
||||
.icon-sales:before {
|
||||
content: '💰';
|
||||
}
|
||||
|
||||
.icon-visits:before {
|
||||
content: '👁️';
|
||||
}
|
||||
|
||||
.icon-orders:before {
|
||||
content: '📦';
|
||||
}
|
||||
|
||||
.icon-users:before {
|
||||
content: '👥';
|
||||
}
|
||||
</style>
|
||||
13
mall/pages/mall/admin/index_new.uvue
Normal file
13
mall/pages/mall/admin/index_new.uvue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<AdminLayout />
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
// AdminLayout 现在采用内部路由模式
|
||||
// 所有页面内容在 AdminLayout 内部切换,不再使用 uni.navigateTo
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
24
mall/pages/mall/admin/maintain/data/city-data.uvue
Normal file
24
mall/pages/mall/admin/maintain/data/city-data.uvue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="data-city-data">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">城市数据</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 城市数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.page { padding: 16px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
.tip { color: #999; margin-top: 8px; display: block; }
|
||||
</style>
|
||||
|
||||
21
mall/pages/mall/admin/maintain/data/clear-data.uvue
Normal file
21
mall/pages/mall/admin/maintain/data/clear-data.uvue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="data-clear-data">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">清除数据</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 清除数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
<style scoped>
|
||||
.page { padding: 16px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
.tip { color: #999; margin-top: 8px; display: block; }
|
||||
</style>
|
||||
|
||||
21
mall/pages/mall/admin/maintain/data/logistics-company.uvue
Normal file
21
mall/pages/mall/admin/maintain/data/logistics-company.uvue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="data-logistics-company">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">物流公司</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 物流公司</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
<style scoped>
|
||||
.page { padding: 16px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
.tip { color: #999; margin-top: 8px; display: block; }
|
||||
</style>
|
||||
|
||||
22
mall/pages/mall/admin/maintain/dev-config/category.uvue
Normal file
22
mall/pages/mall/admin/maintain/dev-config/category.uvue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="dev-config-category">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">配置分类</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 配置分类</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
<style scoped>
|
||||
.page { padding: 16px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
.tip { color: #999; margin-top: 8px; display: block; }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="dev-config-combo">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">组合数据</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 组合数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 16px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
.tip { color: #999; margin-top: 8px; display: block; }
|
||||
</style>
|
||||
|
||||
22
mall/pages/mall/admin/maintain/dev-config/cron-job.uvue
Normal file
22
mall/pages/mall/admin/maintain/dev-config/cron-job.uvue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="dev-config-cron">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">定时任务</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 定时任务</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 16px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
.tip { color: #999; margin-top: 8px; display: block; }
|
||||
</style>
|
||||
|
||||
22
mall/pages/mall/admin/maintain/dev-config/custom-event.uvue
Normal file
22
mall/pages/mall/admin/maintain/dev-config/custom-event.uvue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="dev-config-event">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">自定事件</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 自定事件</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 16px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
.tip { color: #999; margin-top: 8px; display: block; }
|
||||
</style>
|
||||
|
||||
22
mall/pages/mall/admin/maintain/dev-config/module-config.uvue
Normal file
22
mall/pages/mall/admin/maintain/dev-config/module-config.uvue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="dev-config-module">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">模块配置</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 模块配置</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 16px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
.tip { color: #999; margin-top: 8px; display: block; }
|
||||
</style>
|
||||
|
||||
22
mall/pages/mall/admin/maintain/dev-config/permission.uvue
Normal file
22
mall/pages/mall/admin/maintain/dev-config/permission.uvue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="dev-config-permission">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">权限维护</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 权限维护</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { padding: 16px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
.tip { color: #999; margin-top: 8px; display: block; }
|
||||
</style>
|
||||
|
||||
21
mall/pages/mall/admin/maintain/dev-tools/api.uvue
Normal file
21
mall/pages/mall/admin/maintain/dev-tools/api.uvue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="dev-tools-api">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">接口管理</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 接口管理</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script>
|
||||
<style scoped>
|
||||
.page { padding: 16px; }
|
||||
.title { font-size: 18px; font-weight: 600; }
|
||||
.tip { color: #999; margin-top: 8px; display: block; }
|
||||
</style>
|
||||
|
||||
13
mall/pages/mall/admin/maintain/dev-tools/codegen.uvue
Normal file
13
mall/pages/mall/admin/maintain/dev-tools/codegen.uvue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="dev-tools-codegen">
|
||||
<view class="page">
|
||||
|
||||
<view class="header">
|
||||
<text class="title">代码生成</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 代码生成</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
12
mall/pages/mall/admin/maintain/dev-tools/data-dict.uvue
Normal file
12
mall/pages/mall/admin/maintain/dev-tools/data-dict.uvue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="dev-tools-dict">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">数据字典</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 数据字典</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
12
mall/pages/mall/admin/maintain/dev-tools/database.uvue
Normal file
12
mall/pages/mall/admin/maintain/dev-tools/database.uvue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="dev-tools-db">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">数据库管理</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 数据库管理</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
12
mall/pages/mall/admin/maintain/dev-tools/file.uvue
Normal file
12
mall/pages/mall/admin/maintain/dev-tools/file.uvue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="dev-tools-file">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">文件管理</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 文件管理</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
13
mall/pages/mall/admin/maintain/external/account.uvue
vendored
Normal file
13
mall/pages/mall/admin/maintain/external/account.uvue
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="external-account">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">账号管理</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 账号管理</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
|
||||
</template>
|
||||
12
mall/pages/mall/admin/maintain/i18n/language-detail.uvue
Normal file
12
mall/pages/mall/admin/maintain/i18n/language-detail.uvue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="i18n-language-detail">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">语言详情</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 语言详情</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
12
mall/pages/mall/admin/maintain/i18n/language-list.uvue
Normal file
12
mall/pages/mall/admin/maintain/i18n/language-list.uvue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="i18n-language-list">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">语言列表</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 语言列表</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
12
mall/pages/mall/admin/maintain/i18n/region-list.uvue
Normal file
12
mall/pages/mall/admin/maintain/i18n/region-list.uvue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="i18n-region-list">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">地区列表</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 地区列表</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
12
mall/pages/mall/admin/maintain/i18n/translate-config.uvue
Normal file
12
mall/pages/mall/admin/maintain/i18n/translate-config.uvue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="i18n-translate-config">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">翻译配置</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 翻译配置</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
12
mall/pages/mall/admin/maintain/security/online-upgrade.uvue
Normal file
12
mall/pages/mall/admin/maintain/security/online-upgrade.uvue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="security-online-upgrade">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">在线升级</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 在线升级</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
12
mall/pages/mall/admin/maintain/security/refresh-cache.uvue
Normal file
12
mall/pages/mall/admin/maintain/security/refresh-cache.uvue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="security-refresh-cache">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">刷新缓存</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 刷新缓存</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
12
mall/pages/mall/admin/maintain/security/system-log.uvue
Normal file
12
mall/pages/mall/admin/maintain/security/system-log.uvue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="security-system-log">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">系统日志</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 系统日志</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
17
mall/pages/mall/admin/maintain/system-info.uvue
Normal file
17
mall/pages/mall/admin/maintain/system-info.uvue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="system-info">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">系统信息</text>
|
||||
</view>
|
||||
<view class="content">
|
||||
<text class="tip">TODO: 系统信息</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
</script> <style scoped> .page { padding: 16px; } .title { font-size: 18px; font-weight: 600; } .tip { color: #999; margin-top: 8px; display: block; } </style>
|
||||
|
||||
64
mall/pages/mall/admin/marketing-management.uvue
Normal file
64
mall/pages/mall/admin/marketing-management.uvue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="marketing">
|
||||
<view class="Page">
|
||||
<view class="Header">
|
||||
<text class="Title">营销管理</text>
|
||||
<text class="SubTitle">marketing-management</text>
|
||||
</view>
|
||||
|
||||
<view class="Card">
|
||||
<text class="Label">页面参数(query)</text>
|
||||
<text class="Mono">{{ params }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
const params = ref('')
|
||||
|
||||
onLoad((options) => {
|
||||
// options: Record<string, any>
|
||||
params.value = JSON.stringify(options ?? {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.Page {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.Header {
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.SubTitle {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.Card {
|
||||
margin-top: 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.Mono {
|
||||
font-size: 24rpx;
|
||||
font-family: monospace;
|
||||
line-height: 36rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
81
mall/pages/mall/admin/marketing/bargain/list.uvue
Normal file
81
mall/pages/mall/admin/marketing/bargain/list.uvue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">砍价活动</text>
|
||||
<text class="page-subtitle">Component: MarketingBargain</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 砍价活动 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
81
mall/pages/mall/admin/marketing/combination/list.uvue
Normal file
81
mall/pages/mall/admin/marketing/combination/list.uvue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">拼团活动</text>
|
||||
<text class="page-subtitle">Component: MarketingCombination</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 拼团活动 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
65
mall/pages/mall/admin/marketing/coupon/list.uvue
Normal file
65
mall/pages/mall/admin/marketing/coupon/list.uvue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="coupon-list">
|
||||
<view class="page">
|
||||
<view class="Header">
|
||||
<text class="Title">优惠券列表</text>
|
||||
<text class="SubTitle">marketing/coupon/list</text>
|
||||
</view>
|
||||
|
||||
<view class="Card">
|
||||
<text class="Label">页面参数(query)</text>
|
||||
<text class="Mono">{{ params }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
|
||||
const params = ref('')
|
||||
|
||||
onLoad((options) => {
|
||||
// options: Record<string, any>
|
||||
params.value = JSON.stringify(options ?? {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.Page {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.Header {
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.SubTitle {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.Card {
|
||||
margin-top: 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.Mono {
|
||||
font-size: 24rpx;
|
||||
font-family: monospace;
|
||||
line-height: 36rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
65
mall/pages/mall/admin/marketing/coupon/receive.uvue
Normal file
65
mall/pages/mall/admin/marketing/coupon/receive.uvue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<AdminLayout currentPage="coupon-receive">
|
||||
<view class="page">
|
||||
<view class="Header">
|
||||
<text class="Title">领取情况</text>
|
||||
<text class="SubTitle">marketing/coupon/receive</text>
|
||||
</view>
|
||||
|
||||
<view class="Card">
|
||||
<text class="Label">页面参数(query)</text>
|
||||
<text class="Mono">{{ params }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
|
||||
|
||||
const params = ref('')
|
||||
|
||||
onLoad((options) => {
|
||||
// options: Record<string, any>
|
||||
params.value = JSON.stringify(options ?? {})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.Page {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.Header {
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.SubTitle {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.Card {
|
||||
margin-top: 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.Label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.Mono {
|
||||
font-size: 24rpx;
|
||||
font-family: monospace;
|
||||
line-height: 36rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
27
mall/pages/mall/admin/marketing/groupbuy/goods.uvue
Normal file
27
mall/pages/mall/admin/marketing/groupbuy/goods.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('groupbuy-goods')
|
||||
const title = ref<string>('goods')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/groupbuy/list.uvue
Normal file
27
mall/pages/mall/admin/marketing/groupbuy/list.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('groupbuy-list')
|
||||
const title = ref<string>('list')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
26
mall/pages/mall/admin/marketing/index.uvue
Normal file
26
mall/pages/mall/admin/marketing/index.uvue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('marketing')
|
||||
const title = ref<string>('营销看板')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
81
mall/pages/mall/admin/marketing/integral/list.uvue
Normal file
81
mall/pages/mall/admin/marketing/integral/list.uvue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">积分管理</text>
|
||||
<text class="page-subtitle">Component: MarketingIntegral</text>
|
||||
</view>
|
||||
|
||||
<view class="page-content">
|
||||
<view class="placeholder-card">
|
||||
<text class="placeholder-title">页面占位</text>
|
||||
<text class="placeholder-desc">该功能模块正在开发中</text>
|
||||
<text class="placeholder-info">当前采用 CRMEB 路由体系 1:1 映射</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// TODO: 实现 积分管理 的具体功能
|
||||
const loading = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.placeholder-card {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.placeholder-desc {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-info {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
27
mall/pages/mall/admin/marketing/live/anchor.uvue
Normal file
27
mall/pages/mall/admin/marketing/live/anchor.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('live-anchor')
|
||||
const title = ref<string>('anchor')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/live/goods.uvue
Normal file
27
mall/pages/mall/admin/marketing/live/goods.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('live-goods')
|
||||
const title = ref<string>('goods')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/live/room.uvue
Normal file
27
mall/pages/mall/admin/marketing/live/room.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('live-room')
|
||||
const title = ref<string>('room')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/lottery/config.uvue
Normal file
27
mall/pages/mall/admin/marketing/lottery/config.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('lottery-config')
|
||||
const title = ref<string>('config')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/lottery/list.uvue
Normal file
27
mall/pages/mall/admin/marketing/lottery/list.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('lottery-list')
|
||||
const title = ref<string>('list')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/member/card.uvue
Normal file
27
mall/pages/mall/admin/marketing/member/card.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('member-card')
|
||||
const title = ref<string>('card')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/member/config.uvue
Normal file
27
mall/pages/mall/admin/marketing/member/config.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('member-config')
|
||||
const title = ref<string>('config')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/member/record.uvue
Normal file
27
mall/pages/mall/admin/marketing/member/record.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('member-record')
|
||||
const title = ref<string>('record')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/member/rights.uvue
Normal file
27
mall/pages/mall/admin/marketing/member/rights.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('member-rights')
|
||||
const title = ref<string>('rights')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/member/type.uvue
Normal file
27
mall/pages/mall/admin/marketing/member/type.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('member-type')
|
||||
const title = ref<string>('type')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/newcomer.uvue
Normal file
27
mall/pages/mall/admin/marketing/newcomer.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('newcomer')
|
||||
const title = ref<string>('newcomer')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
27
mall/pages/mall/admin/marketing/points/config.uvue
Normal file
27
mall/pages/mall/admin/marketing/points/config.uvue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<AdminLayout :currentPage="currentPage">
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="title">{{ title }}</text>
|
||||
<text class="sub-title">页面占位 (自动生成)</text>
|
||||
</view>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||||
const currentPage = ref<string>('points-config')
|
||||
const title = ref<string>('config')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/uni.scss';
|
||||
.page { padding: $space-lg; }
|
||||
.header { padding: $space-lg; border-radius: $radius; background: $background-primary; box-shadow: $shadow-xs; }
|
||||
.title { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $text-primary; }
|
||||
.sub-title { margin-top: $space-xs; font-size: $font-size-md; color: $text-secondary; }
|
||||
</style>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user