feat: add post page
This commit is contained in:
@@ -64,15 +64,14 @@ func (m *mediasModel) countByCondition(ctx context.Context, expr BoolExpression)
|
||||
}
|
||||
|
||||
func (m *mediasModel) List(ctx context.Context, pagination *requests.Pagination) (*requests.Pager, error) {
|
||||
limit := pagination.GetLimit()
|
||||
offset := pagination.GetOffset()
|
||||
pagination.Format()
|
||||
|
||||
tbl := table.Medias
|
||||
stmt := tbl.
|
||||
SELECT(tbl.AllColumns).
|
||||
ORDER_BY(tbl.ID.DESC()).
|
||||
LIMIT(limit).
|
||||
OFFSET(offset)
|
||||
LIMIT(pagination.Limit).
|
||||
OFFSET(pagination.Offset)
|
||||
m.log.Infof("sql: %s", stmt.DebugSql())
|
||||
|
||||
var medias []model.Medias
|
||||
|
||||
@@ -3,6 +3,7 @@ package models
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"quyun/app/requests"
|
||||
"quyun/database/fields"
|
||||
@@ -72,6 +73,9 @@ func (m *postsModel) GetByID(ctx context.Context, id int64) (*model.Posts, error
|
||||
|
||||
// Create
|
||||
func (m *postsModel) Create(ctx context.Context, model *model.Posts) error {
|
||||
model.CreatedAt = time.Now()
|
||||
model.UpdatedAt = time.Now()
|
||||
|
||||
tbl := table.Posts
|
||||
stmt := tbl.INSERT(tbl.MutableColumns).MODEL(model)
|
||||
m.log.Infof("sql: %s", stmt.DebugSql())
|
||||
@@ -86,6 +90,8 @@ func (m *postsModel) Create(ctx context.Context, model *model.Posts) error {
|
||||
|
||||
// Update
|
||||
func (m *postsModel) Update(ctx context.Context, id int64, model *model.Posts) error {
|
||||
model.UpdatedAt = time.Now()
|
||||
|
||||
tbl := table.Posts
|
||||
stmt := tbl.UPDATE(tbl.MutableColumns).SET(model).WHERE(tbl.ID.EQ(Int64(id)))
|
||||
m.log.Infof("sql: %s", stmt.DebugSql())
|
||||
@@ -118,16 +124,15 @@ func (m *postsModel) countByCondition(ctx context.Context, expr BoolExpression)
|
||||
}
|
||||
|
||||
func (m *postsModel) List(ctx context.Context, pagination *requests.Pagination, cond BoolExpression) (*requests.Pager, error) {
|
||||
limit := pagination.GetLimit()
|
||||
offset := pagination.GetOffset()
|
||||
pagination.Format()
|
||||
|
||||
tbl := table.Posts
|
||||
stmt := tbl.
|
||||
SELECT(tbl.AllColumns).
|
||||
WHERE(cond).
|
||||
ORDER_BY(tbl.ID.DESC()).
|
||||
LIMIT(limit).
|
||||
OFFSET(offset)
|
||||
LIMIT(pagination.Limit).
|
||||
OFFSET(pagination.Offset)
|
||||
m.log.Infof("sql: %s", stmt.DebugSql())
|
||||
|
||||
var posts []model.Posts
|
||||
|
||||
@@ -2,10 +2,14 @@ package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"quyun/app/service/testx"
|
||||
"quyun/database"
|
||||
"quyun/database/fields"
|
||||
"quyun/database/schemas/public/model"
|
||||
"quyun/database/schemas/public/table"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
@@ -36,8 +40,37 @@ func Test_Posts(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *PostsTestSuite) Test_Demo() {
|
||||
func (s *PostsTestSuite) Test_BatchInsert() {
|
||||
Convey("Test_Demo", s.T(), func() {
|
||||
database.Truncate(context.Background(), db, table.Posts.TableName())
|
||||
|
||||
count := 100
|
||||
for i := 0; i < count; i++ {
|
||||
model := model.Posts{
|
||||
Status: fields.PostStatusPublished,
|
||||
Title: fmt.Sprintf("test-title-%d", i),
|
||||
Description: fmt.Sprintf("test-description-%d", i),
|
||||
Content: fmt.Sprintf("test-content-%d", i),
|
||||
Price: rand.Int63n(10000),
|
||||
Discount: int16(rand.Intn(100)),
|
||||
Views: rand.Int63n(10000),
|
||||
Likes: rand.Int63n(10000),
|
||||
Tags: fields.ToJson([]string{"tag1", "tag2", "tag3"}),
|
||||
Assets: fields.ToJson([]fields.MediaAsset{
|
||||
{
|
||||
Type: fields.MediaAssetTypeAudio,
|
||||
Media: rand.Int63n(10000),
|
||||
},
|
||||
{
|
||||
Type: fields.MediaAssetTypeVideo,
|
||||
Media: rand.Int63n(10000),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
if err := Posts.Create(context.Background(), &model); err != nil {
|
||||
s.T().Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,29 +9,19 @@ type Pager struct {
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Page int64 `json:"page" form:"page" query:"page"`
|
||||
Limit int64 `json:"limit" form:"limit" query:"limit"`
|
||||
Page int64 `json:"page" form:"page" query:"page"`
|
||||
Limit int64 `json:"limit" form:"limit" query:"limit"`
|
||||
Offset int64 `json:"-"`
|
||||
}
|
||||
|
||||
func (filter *Pagination) GetOffset() int64 {
|
||||
return (filter.Page - 1) * filter.Limit
|
||||
}
|
||||
|
||||
func (filter *Pagination) GetLimit() int64 {
|
||||
if filter.Limit <= 0 {
|
||||
return 10
|
||||
}
|
||||
return filter.Limit
|
||||
}
|
||||
|
||||
func (filter *Pagination) Format() *Pagination {
|
||||
func (filter *Pagination) Format() {
|
||||
if filter.Page <= 0 {
|
||||
filter.Page = 1
|
||||
}
|
||||
|
||||
if !lo.Contains([]int64{10, 20, 50, 100}, filter.Limit) {
|
||||
if !lo.Contains([]int64{10, 25, 50, 100}, filter.Limit) {
|
||||
filter.Limit = 10
|
||||
}
|
||||
|
||||
return filter
|
||||
filter.Offset = (filter.Page - 1) * filter.Limit
|
||||
}
|
||||
|
||||
@@ -35,4 +35,8 @@ Content-Type: application/json
|
||||
|
||||
### get medias
|
||||
GET {{host}}/v1/admin/medias HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
### get posts
|
||||
GET {{host}}/v1/admin/posts HTTP/1.1
|
||||
Content-Type: application/json
|
||||
12
frontend/admin/src/api/postService.js
Normal file
12
frontend/admin/src/api/postService.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import httpClient from './httpClient';
|
||||
|
||||
export const postService = {
|
||||
getPosts({ page = 1, limit = 10 } = {}) {
|
||||
return httpClient.get('/admin/posts', {
|
||||
params: { page, limit }
|
||||
});
|
||||
},
|
||||
getPost(id) {
|
||||
return httpClient.get(`/admin/posts/${id}`);
|
||||
},
|
||||
};
|
||||
297
frontend/admin/src/api/post_list.json
Normal file
297
frontend/admin/src/api/post_list.json
Normal file
@@ -0,0 +1,297 @@
|
||||
{
|
||||
"page": 1,
|
||||
"limit": 10,
|
||||
"total": 100,
|
||||
"items": [
|
||||
{
|
||||
"id": 100,
|
||||
"created_at": "2025-04-09T14:42:08.721793Z",
|
||||
"updated_at": "2025-04-09T14:42:08.721793Z",
|
||||
"deleted_at": null,
|
||||
"status": 1,
|
||||
"title": "test-title-99",
|
||||
"description": "test-description-99",
|
||||
"content": "test-content-99",
|
||||
"price": 4205,
|
||||
"discount": 45,
|
||||
"views": 8566,
|
||||
"likes": 2912,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"type": "audio",
|
||||
"media": 8775
|
||||
},
|
||||
{
|
||||
"type": "video",
|
||||
"media": 2905
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 99,
|
||||
"created_at": "2025-04-09T14:42:08.715622Z",
|
||||
"updated_at": "2025-04-09T14:42:08.715622Z",
|
||||
"deleted_at": null,
|
||||
"status": 1,
|
||||
"title": "test-title-98",
|
||||
"description": "test-description-98",
|
||||
"content": "test-content-98",
|
||||
"price": 8407,
|
||||
"discount": 59,
|
||||
"views": 4779,
|
||||
"likes": 1514,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"type": "audio",
|
||||
"media": 5265
|
||||
},
|
||||
{
|
||||
"type": "video",
|
||||
"media": 9715
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 98,
|
||||
"created_at": "2025-04-09T14:42:08.710788Z",
|
||||
"updated_at": "2025-04-09T14:42:08.710788Z",
|
||||
"deleted_at": null,
|
||||
"status": 1,
|
||||
"title": "test-title-97",
|
||||
"description": "test-description-97",
|
||||
"content": "test-content-97",
|
||||
"price": 1314,
|
||||
"discount": 15,
|
||||
"views": 6962,
|
||||
"likes": 440,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"type": "audio",
|
||||
"media": 1321
|
||||
},
|
||||
{
|
||||
"type": "video",
|
||||
"media": 6559
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 97,
|
||||
"created_at": "2025-04-09T14:42:08.705116Z",
|
||||
"updated_at": "2025-04-09T14:42:08.705116Z",
|
||||
"deleted_at": null,
|
||||
"status": 1,
|
||||
"title": "test-title-96",
|
||||
"description": "test-description-96",
|
||||
"content": "test-content-96",
|
||||
"price": 9030,
|
||||
"discount": 14,
|
||||
"views": 6942,
|
||||
"likes": 9185,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"type": "audio",
|
||||
"media": 6482
|
||||
},
|
||||
{
|
||||
"type": "video",
|
||||
"media": 2153
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 96,
|
||||
"created_at": "2025-04-09T14:42:08.697806Z",
|
||||
"updated_at": "2025-04-09T14:42:08.697806Z",
|
||||
"deleted_at": null,
|
||||
"status": 1,
|
||||
"title": "test-title-95",
|
||||
"description": "test-description-95",
|
||||
"content": "test-content-95",
|
||||
"price": 6363,
|
||||
"discount": 41,
|
||||
"views": 8679,
|
||||
"likes": 1755,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"type": "audio",
|
||||
"media": 6229
|
||||
},
|
||||
{
|
||||
"type": "video",
|
||||
"media": 3559
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 95,
|
||||
"created_at": "2025-04-09T14:42:08.693557Z",
|
||||
"updated_at": "2025-04-09T14:42:08.693557Z",
|
||||
"deleted_at": null,
|
||||
"status": 1,
|
||||
"title": "test-title-94",
|
||||
"description": "test-description-94",
|
||||
"content": "test-content-94",
|
||||
"price": 6819,
|
||||
"discount": 61,
|
||||
"views": 9728,
|
||||
"likes": 1103,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"type": "audio",
|
||||
"media": 6464
|
||||
},
|
||||
{
|
||||
"type": "video",
|
||||
"media": 4964
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 94,
|
||||
"created_at": "2025-04-09T14:42:08.688664Z",
|
||||
"updated_at": "2025-04-09T14:42:08.688664Z",
|
||||
"deleted_at": null,
|
||||
"status": 1,
|
||||
"title": "test-title-93",
|
||||
"description": "test-description-93",
|
||||
"content": "test-content-93",
|
||||
"price": 4324,
|
||||
"discount": 0,
|
||||
"views": 473,
|
||||
"likes": 2956,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"type": "audio",
|
||||
"media": 3043
|
||||
},
|
||||
{
|
||||
"type": "video",
|
||||
"media": 1234
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 93,
|
||||
"created_at": "2025-04-09T14:42:08.681727Z",
|
||||
"updated_at": "2025-04-09T14:42:08.681727Z",
|
||||
"deleted_at": null,
|
||||
"status": 1,
|
||||
"title": "test-title-92",
|
||||
"description": "test-description-92",
|
||||
"content": "test-content-92",
|
||||
"price": 8068,
|
||||
"discount": 86,
|
||||
"views": 3783,
|
||||
"likes": 3477,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"type": "audio",
|
||||
"media": 3623
|
||||
},
|
||||
{
|
||||
"type": "video",
|
||||
"media": 2628
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 92,
|
||||
"created_at": "2025-04-09T14:42:08.677622Z",
|
||||
"updated_at": "2025-04-09T14:42:08.677622Z",
|
||||
"deleted_at": null,
|
||||
"status": 1,
|
||||
"title": "test-title-91",
|
||||
"description": "test-description-91",
|
||||
"content": "test-content-91",
|
||||
"price": 6313,
|
||||
"discount": 43,
|
||||
"views": 982,
|
||||
"likes": 2660,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"type": "audio",
|
||||
"media": 3542
|
||||
},
|
||||
{
|
||||
"type": "video",
|
||||
"media": 6605
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 91,
|
||||
"created_at": "2025-04-09T14:42:08.674006Z",
|
||||
"updated_at": "2025-04-09T14:42:08.674006Z",
|
||||
"deleted_at": null,
|
||||
"status": 1,
|
||||
"title": "test-title-90",
|
||||
"description": "test-description-90",
|
||||
"content": "test-content-90",
|
||||
"price": 3659,
|
||||
"discount": 83,
|
||||
"views": 76,
|
||||
"likes": 4807,
|
||||
"tags": [
|
||||
"tag1",
|
||||
"tag2",
|
||||
"tag3"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"type": "audio",
|
||||
"media": 3191
|
||||
},
|
||||
{
|
||||
"type": "video",
|
||||
"media": 2689
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup>
|
||||
import { postService } from '@/api/postService'; // Assuming you have a postService for API calls
|
||||
import { InputText } from 'primevue';
|
||||
import Badge from 'primevue/badge';
|
||||
|
||||
import Button from 'primevue/button';
|
||||
import Column from 'primevue/column';
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
@@ -18,22 +20,6 @@ const router = useRouter();
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
|
||||
// State for edit dialog (removed "create" functionality since we now have a dedicated page)
|
||||
const postDialog = ref(false);
|
||||
const postDialogTitle = ref('编辑文章');
|
||||
const editMode = ref(true); // Always true now since we only use dialog for editing
|
||||
const currentPost = ref({
|
||||
id: null,
|
||||
title: '',
|
||||
author: '',
|
||||
thumbnail: '',
|
||||
price: 0,
|
||||
publishedAt: '',
|
||||
status: '',
|
||||
mediaTypes: [],
|
||||
viewCount: 0
|
||||
});
|
||||
|
||||
// Post statuses for filtering
|
||||
const statusOptions = ref([
|
||||
{ name: '所有状态', value: null },
|
||||
@@ -50,46 +36,34 @@ const mediaTypeOptions = ref([
|
||||
{ name: '音频', value: '音频' }
|
||||
]);
|
||||
|
||||
const selectedStatus = ref(statusOptions.value[0]);
|
||||
const globalFilterValue = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
// Sample data - in a real app, this would come from an API
|
||||
const posts = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '如何高效学习编程',
|
||||
author: '张三',
|
||||
thumbnail: 'https://via.placeholder.com/150',
|
||||
price: 29.99,
|
||||
publishedAt: '2023-06-15 14:30',
|
||||
status: '已发布',
|
||||
mediaTypes: ['文章', '视频'],
|
||||
viewCount: 1254
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '前端开发最佳实践',
|
||||
author: '李四',
|
||||
thumbnail: 'https://via.placeholder.com/150',
|
||||
price: 49.99,
|
||||
publishedAt: '2023-06-10 09:15',
|
||||
status: '草稿',
|
||||
mediaTypes: ['文章'],
|
||||
viewCount: 789
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '数据分析入门指南',
|
||||
author: '王五',
|
||||
thumbnail: 'https://via.placeholder.com/150',
|
||||
price: 0.00,
|
||||
publishedAt: '2023-06-05 16:45',
|
||||
status: '已下架',
|
||||
mediaTypes: ['文章', '音频'],
|
||||
viewCount: 2567
|
||||
}
|
||||
]);
|
||||
const posts = ref([]);
|
||||
|
||||
// Pagination state
|
||||
const page = ref(0); // 改为从0开始计数
|
||||
const limit = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
// Status mapping
|
||||
const statusMap = {
|
||||
1: '已发布',
|
||||
2: '草稿',
|
||||
3: '已下架'
|
||||
};
|
||||
|
||||
// Transform assets to media types
|
||||
const getMediaTypes = (assets) => {
|
||||
return [...new Set(assets.map(asset => {
|
||||
switch (asset.type) {
|
||||
case 'audio': return '音频';
|
||||
case 'video': return '视频';
|
||||
default: return '文章';
|
||||
}
|
||||
}))];
|
||||
};
|
||||
|
||||
// Navigate to post creation page
|
||||
const navigateToCreatePost = () => {
|
||||
@@ -122,17 +96,41 @@ const confirmDelete = (post) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch posts data
|
||||
// Format datetime to YY/MM/DD HH:mm:ss
|
||||
const formatDateTime = (dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
}).replace(/\//g, '-');
|
||||
};
|
||||
|
||||
// Calculate price after discount
|
||||
const calculateDiscountPrice = (price, discount) => {
|
||||
if (!discount) return price;
|
||||
return price * (1 - discount / 100);
|
||||
};
|
||||
|
||||
// Fetch posts data with pagination
|
||||
const fetchPosts = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// In a real app, this would be an API call
|
||||
// const response = await postApi.getPosts();
|
||||
// posts.value = response.data;
|
||||
|
||||
// Simulate API delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
// Using sample data already defined above
|
||||
const response = await postService.getPosts(page.value + 1, limit.value); // API调用时页码加1
|
||||
posts.value = response.items.map(post => ({
|
||||
...post,
|
||||
status: statusMap[post.status] || '未知',
|
||||
mediaTypes: getMediaTypes(post.assets),
|
||||
price: post.price / 100, // Convert cents to yuan
|
||||
publishedAt: formatDateTime(post.created_at),
|
||||
viewCount: post.views,
|
||||
likes: post.likes
|
||||
}));
|
||||
total.value = response.total;
|
||||
} catch (error) {
|
||||
toast.add({ severity: 'error', summary: '错误', detail: '加载文章失败', life: 3000 });
|
||||
} finally {
|
||||
@@ -140,6 +138,13 @@ const fetchPosts = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle page change
|
||||
const onPage = (event) => {
|
||||
page.value = event.page; // PrimeVue的页码从0开始
|
||||
limit.value = event.rows;
|
||||
fetchPosts();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchPosts();
|
||||
});
|
||||
@@ -183,11 +188,12 @@ const formatMediaTypes = (mediaTypes) => {
|
||||
<InputText v-model="globalFilterValue" placeholder="搜索文章..." class="flex-1" />
|
||||
</div>
|
||||
|
||||
<DataTable v-model:filters="filters" :value="posts" :paginator="true" :rows="5"
|
||||
<DataTable v-model:filters="filters" :value="posts" :paginator="true" :rows="limit" :totalRecords="total"
|
||||
:loading="loading" :lazy="true" v-model:first="page" @page="onPage"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
:rowsPerPageOptions="[5, 10, 25]"
|
||||
currentPageReportTemplate="显示第 {first} 到 {last} 条,共 {totalRecords} 条结果" :loading="loading" dataKey="id"
|
||||
:globalFilterFields="['title', 'author', 'status', 'mediaTypes']"
|
||||
:rowsPerPageOptions="[10, 20, 50]"
|
||||
currentPageReportTemplate="显示第 {first} 到 {last} 条,共 {totalRecords} 条结果" dataKey="id"
|
||||
:globalFilterFields="['title', 'description', 'status']"
|
||||
:filters="{ global: { value: globalFilterValue, matchMode: 'contains' } }" stripedRows removableSort
|
||||
class="p-datatable-sm" responsiveLayout="scroll">
|
||||
|
||||
@@ -212,11 +218,25 @@ const formatMediaTypes = (mediaTypes) => {
|
||||
|
||||
<Column field="price" header="价格" sortable>
|
||||
<template #body="{ data }">
|
||||
<div class="text-sm text-gray-900">{{ formatPrice(data.price) }}</div>
|
||||
<div class="text-sm text-gray-900">
|
||||
<span class="line-through text-gray-500" v-if="data.discount">
|
||||
{{ formatPrice(data.price) }}
|
||||
</span>
|
||||
<span :class="{ 'ml-2': data.discount }">
|
||||
{{ formatPrice(calculateDiscountPrice(data.price, data.discount)) }}
|
||||
</span>
|
||||
<span v-if="data.discount" class="ml-2 text-red-500">
|
||||
(-{{ data.discount }}%)
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="publishedAt" header="发布时间" sortable></Column>
|
||||
<Column field="publishedAt" header="发布时间" sortable>
|
||||
<template #body="{ data }">
|
||||
<div class="text-sm text-gray-900">{{ data.publishedAt }}</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="status" header="发布状态" sortable>
|
||||
<template #body="{ data }">
|
||||
@@ -246,6 +266,12 @@ const formatMediaTypes = (mediaTypes) => {
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="likes" header="点赞数" sortable>
|
||||
<template #body="{ data }">
|
||||
<div class="text-sm text-gray-500">{{ data.likes }}</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="操作" :exportable="false" style="min-width:8rem">
|
||||
<template #body="{ data }">
|
||||
<div class="flex justify-center space-x-2">
|
||||
|
||||
Reference in New Issue
Block a user