mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 19:37:33 +08:00
541 lines
16 KiB
Vue
541 lines
16 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="space-y-4">
|
|||
|
|
<!-- 搜索和筛选 -->
|
|||
|
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
|||
|
|
<n-input
|
|||
|
|
v-model:value="searchQuery"
|
|||
|
|
placeholder="搜索未转存资源..."
|
|||
|
|
@keyup.enter="handleSearch"
|
|||
|
|
clearable
|
|||
|
|
>
|
|||
|
|
<template #prefix>
|
|||
|
|
<i class="fas fa-search"></i>
|
|||
|
|
</template>
|
|||
|
|
</n-input>
|
|||
|
|
|
|||
|
|
<n-select
|
|||
|
|
v-model:value="selectedCategory"
|
|||
|
|
placeholder="选择分类"
|
|||
|
|
:options="categoryOptions"
|
|||
|
|
clearable
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<n-select
|
|||
|
|
v-model:value="selectedTag"
|
|||
|
|
placeholder="选择标签"
|
|||
|
|
:options="tagOptions"
|
|||
|
|
clearable
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<n-select
|
|||
|
|
v-model:value="selectedStatus"
|
|||
|
|
placeholder="资源状态"
|
|||
|
|
:options="statusOptions"
|
|||
|
|
clearable
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<n-button type="primary" @click="handleSearch">
|
|||
|
|
<template #icon>
|
|||
|
|
<i class="fas fa-search"></i>
|
|||
|
|
</template>
|
|||
|
|
搜索
|
|||
|
|
</n-button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 批量操作 -->
|
|||
|
|
<n-card>
|
|||
|
|
<div class="flex items-center justify-between">
|
|||
|
|
<div class="flex items-center space-x-4">
|
|||
|
|
<div class="flex items-center space-x-2">
|
|||
|
|
<n-checkbox
|
|||
|
|
:checked="isAllSelected"
|
|||
|
|
@update:checked="toggleSelectAll"
|
|||
|
|
:indeterminate="isIndeterminate"
|
|||
|
|
/>
|
|||
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">全选</span>
|
|||
|
|
</div>
|
|||
|
|
<span class="text-sm text-gray-500">
|
|||
|
|
共 {{ total }} 个资源,已选择 {{ selectedResources.length }} 个
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="flex space-x-2">
|
|||
|
|
<n-button
|
|||
|
|
type="primary"
|
|||
|
|
:disabled="selectedResources.length === 0"
|
|||
|
|
:loading="batchTransferring"
|
|||
|
|
@click="handleBatchTransfer"
|
|||
|
|
>
|
|||
|
|
<template #icon>
|
|||
|
|
<i class="fas fa-exchange-alt"></i>
|
|||
|
|
</template>
|
|||
|
|
批量转存 ({{ selectedResources.length }})
|
|||
|
|
</n-button>
|
|||
|
|
|
|||
|
|
<n-button @click="refreshData">
|
|||
|
|
<template #icon>
|
|||
|
|
<i class="fas fa-refresh"></i>
|
|||
|
|
</template>
|
|||
|
|
刷新
|
|||
|
|
</n-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</n-card>
|
|||
|
|
|
|||
|
|
<!-- 资源列表 -->
|
|||
|
|
<n-card>
|
|||
|
|
<div v-if="loading" class="flex items-center justify-center py-8">
|
|||
|
|
<n-spin size="large" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="resources.length === 0" class="text-center py-8">
|
|||
|
|
<i class="fas fa-inbox text-4xl text-gray-400 mb-4"></i>
|
|||
|
|
<p class="text-gray-500">暂无未转存的夸克资源</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else>
|
|||
|
|
<!-- 虚拟列表 -->
|
|||
|
|
<n-virtual-list
|
|||
|
|
:items="resources"
|
|||
|
|
:item-size="120"
|
|||
|
|
style="max-height: 500px"
|
|||
|
|
container-style="height: 500px;"
|
|||
|
|
>
|
|||
|
|
<template #default="{ item }">
|
|||
|
|
<div class="p-4 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800">
|
|||
|
|
<div class="flex items-start space-x-4">
|
|||
|
|
<!-- 选择框 -->
|
|||
|
|
<div class="pt-2">
|
|||
|
|
<n-checkbox
|
|||
|
|
:checked="selectedResources.includes(item.id)"
|
|||
|
|
@update:checked="(checked) => toggleResourceSelection(item.id, checked)"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 资源信息 -->
|
|||
|
|
<div class="flex-1 min-w-0">
|
|||
|
|
<div class="flex items-start justify-between">
|
|||
|
|
<div class="flex-1 min-w-0">
|
|||
|
|
<!-- 标题和状态 -->
|
|||
|
|
<div class="flex items-center space-x-2 mb-2">
|
|||
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white line-clamp-1">
|
|||
|
|
{{ item.title || '未命名资源' }}
|
|||
|
|
</h3>
|
|||
|
|
<n-tag :type="getStatusType(item)" size="small">
|
|||
|
|
{{ getStatusText(item) }}
|
|||
|
|
</n-tag>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 描述 -->
|
|||
|
|
<p class="text-gray-600 dark:text-gray-400 text-sm line-clamp-2 mb-2">
|
|||
|
|
{{ item.description || '暂无描述' }}
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<!-- 元信息 -->
|
|||
|
|
<div class="flex items-center space-x-4 text-sm text-gray-500">
|
|||
|
|
<span class="flex items-center">
|
|||
|
|
<i class="fas fa-folder mr-1"></i>
|
|||
|
|
{{ item.category_name || '未分类' }}
|
|||
|
|
</span>
|
|||
|
|
<span class="flex items-center">
|
|||
|
|
<i class="fas fa-cloud mr-1"></i>
|
|||
|
|
夸克网盘
|
|||
|
|
</span>
|
|||
|
|
<span class="flex items-center">
|
|||
|
|
<i class="fas fa-eye mr-1"></i>
|
|||
|
|
{{ item.view_count || 0 }} 次浏览
|
|||
|
|
</span>
|
|||
|
|
<span class="flex items-center">
|
|||
|
|
<i class="fas fa-calendar mr-1"></i>
|
|||
|
|
{{ formatDate(item.created_at) }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 原始链接 -->
|
|||
|
|
<div class="mt-2">
|
|||
|
|
<div class="flex items-center space-x-2">
|
|||
|
|
<span class="text-xs text-gray-400">原始链接:</span>
|
|||
|
|
<a
|
|||
|
|
:href="item.url"
|
|||
|
|
target="_blank"
|
|||
|
|
class="text-xs text-blue-500 hover:text-blue-700 truncate max-w-xs"
|
|||
|
|
>
|
|||
|
|
{{ item.url }}
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 操作按钮 -->
|
|||
|
|
<div class="flex flex-col space-y-2 ml-4">
|
|||
|
|
<n-button
|
|||
|
|
size="small"
|
|||
|
|
type="primary"
|
|||
|
|
:loading="item.transferring"
|
|||
|
|
@click="handleSingleTransfer(item)"
|
|||
|
|
>
|
|||
|
|
<template #icon>
|
|||
|
|
<i class="fas fa-exchange-alt"></i>
|
|||
|
|
</template>
|
|||
|
|
{{ item.transferring ? '转存中' : '立即转存' }}
|
|||
|
|
</n-button>
|
|||
|
|
|
|||
|
|
<n-button
|
|||
|
|
size="small"
|
|||
|
|
@click="viewResource(item)"
|
|||
|
|
>
|
|||
|
|
<template #icon>
|
|||
|
|
<i class="fas fa-eye"></i>
|
|||
|
|
</template>
|
|||
|
|
查看详情
|
|||
|
|
</n-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</n-virtual-list>
|
|||
|
|
|
|||
|
|
<!-- 分页 -->
|
|||
|
|
<div class="mt-4 flex justify-center">
|
|||
|
|
<n-pagination
|
|||
|
|
v-model:page="currentPage"
|
|||
|
|
v-model:page-size="pageSize"
|
|||
|
|
:item-count="total"
|
|||
|
|
:page-sizes="[20, 50, 100, 200]"
|
|||
|
|
show-size-picker
|
|||
|
|
show-quick-jumper
|
|||
|
|
@update:page="handlePageChange"
|
|||
|
|
@update:page-size="handlePageSizeChange"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</n-card>
|
|||
|
|
|
|||
|
|
<!-- 转存结果模态框 -->
|
|||
|
|
<n-modal v-model:show="showTransferResult" preset="card" title="转存结果" style="width: 600px">
|
|||
|
|
<div v-if="transferResults.length > 0" class="space-y-4">
|
|||
|
|
<div class="grid grid-cols-3 gap-4">
|
|||
|
|
<div class="text-center p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
|||
|
|
<div class="text-xl font-bold text-green-600">{{ transferSuccessCount }}</div>
|
|||
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">成功</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="text-center p-3 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
|||
|
|
<div class="text-xl font-bold text-red-600">{{ transferFailedCount }}</div>
|
|||
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">失败</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="text-center p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
|||
|
|
<div class="text-xl font-bold text-blue-600">{{ transferResults.length }}</div>
|
|||
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">总计</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="max-h-300 overflow-y-auto">
|
|||
|
|
<div v-for="result in transferResults" :key="result.id" class="p-3 border rounded mb-2">
|
|||
|
|
<div class="flex items-center justify-between">
|
|||
|
|
<div class="flex-1 min-w-0">
|
|||
|
|
<div class="text-sm font-medium truncate">{{ result.title }}</div>
|
|||
|
|
<div class="text-xs text-gray-500 truncate">{{ result.url }}</div>
|
|||
|
|
</div>
|
|||
|
|
<n-tag :type="result.success ? 'success' : 'error'" size="small">
|
|||
|
|
{{ result.success ? '成功' : '失败' }}
|
|||
|
|
</n-tag>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="result.message" class="text-xs text-gray-600 mt-1">
|
|||
|
|
{{ result.message }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</n-modal>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|||
|
|
import { useResourceApi, useCategoryApi, useTagApi } from '~/composables/useApi'
|
|||
|
|
|
|||
|
|
// 数据状态
|
|||
|
|
const loading = ref(false)
|
|||
|
|
const resources = ref([])
|
|||
|
|
const total = ref(0)
|
|||
|
|
const currentPage = ref(1)
|
|||
|
|
const pageSize = ref(20)
|
|||
|
|
|
|||
|
|
// 搜索条件
|
|||
|
|
const searchQuery = ref('')
|
|||
|
|
const selectedCategory = ref(null)
|
|||
|
|
const selectedTag = ref(null)
|
|||
|
|
const selectedStatus = ref(null)
|
|||
|
|
|
|||
|
|
// 选择状态
|
|||
|
|
const selectedResources = ref([])
|
|||
|
|
|
|||
|
|
// 批量操作状态
|
|||
|
|
const batchTransferring = ref(false)
|
|||
|
|
const showTransferResult = ref(false)
|
|||
|
|
const transferResults = ref([])
|
|||
|
|
|
|||
|
|
// 选项数据
|
|||
|
|
const categoryOptions = ref([])
|
|||
|
|
const tagOptions = ref([])
|
|||
|
|
const statusOptions = [
|
|||
|
|
{ label: '有效', value: 'valid' },
|
|||
|
|
{ label: '无效', value: 'invalid' },
|
|||
|
|
{ label: '待验证', value: 'pending' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// API实例
|
|||
|
|
const resourceApi = useResourceApi()
|
|||
|
|
const categoryApi = useCategoryApi()
|
|||
|
|
const tagApi = useTagApi()
|
|||
|
|
|
|||
|
|
// 计算属性
|
|||
|
|
const isAllSelected = computed(() => {
|
|||
|
|
return resources.value.length > 0 && selectedResources.value.length === resources.value.length
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const isIndeterminate = computed(() => {
|
|||
|
|
return selectedResources.value.length > 0 && selectedResources.value.length < resources.value.length
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const transferSuccessCount = computed(() => {
|
|||
|
|
return transferResults.value.filter(r => r.success).length
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const transferFailedCount = computed(() => {
|
|||
|
|
return transferResults.value.filter(r => !r.success).length
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 获取未转存资源(夸克网盘且无save_url)
|
|||
|
|
const fetchUntransferredResources = async () => {
|
|||
|
|
loading.value = true
|
|||
|
|
try {
|
|||
|
|
const params = {
|
|||
|
|
page: currentPage.value,
|
|||
|
|
page_size: pageSize.value,
|
|||
|
|
no_save_url: true, // 筛选没有转存链接的资源
|
|||
|
|
pan_name: 'quark' // 仅夸克网盘资源
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (searchQuery.value) {
|
|||
|
|
params.search = searchQuery.value
|
|||
|
|
}
|
|||
|
|
if (selectedCategory.value) {
|
|||
|
|
params.category_id = selectedCategory.value
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = await resourceApi.getResources(params) as any
|
|||
|
|
console.log('未转存资源结果:', result)
|
|||
|
|
|
|||
|
|
if (result && result.resources) {
|
|||
|
|
resources.value = result.resources
|
|||
|
|
total.value = result.total || 0
|
|||
|
|
} else if (Array.isArray(result)) {
|
|||
|
|
resources.value = result
|
|||
|
|
total.value = result.length
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清空选择
|
|||
|
|
selectedResources.value = []
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取未转存资源失败:', error)
|
|||
|
|
resources.value = []
|
|||
|
|
total.value = 0
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取分类选项
|
|||
|
|
const fetchCategories = async () => {
|
|||
|
|
try {
|
|||
|
|
const result = await categoryApi.getCategories() as any
|
|||
|
|
if (result && result.items) {
|
|||
|
|
categoryOptions.value = result.items.map((item: any) => ({
|
|||
|
|
label: item.name,
|
|||
|
|
value: item.id
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取分类失败:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取标签选项
|
|||
|
|
const fetchTags = async () => {
|
|||
|
|
try {
|
|||
|
|
const result = await tagApi.getTags() as any
|
|||
|
|
if (result && result.items) {
|
|||
|
|
tagOptions.value = result.items.map((item: any) => ({
|
|||
|
|
label: item.name,
|
|||
|
|
value: item.id
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取标签失败:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 搜索处理
|
|||
|
|
const handleSearch = () => {
|
|||
|
|
currentPage.value = 1
|
|||
|
|
fetchUntransferredResources()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 刷新数据
|
|||
|
|
const refreshData = () => {
|
|||
|
|
fetchUntransferredResources()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 分页处理
|
|||
|
|
const handlePageChange = (page: number) => {
|
|||
|
|
currentPage.value = page
|
|||
|
|
fetchUntransferredResources()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handlePageSizeChange = (size: number) => {
|
|||
|
|
pageSize.value = size
|
|||
|
|
currentPage.value = 1
|
|||
|
|
fetchUntransferredResources()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 选择处理
|
|||
|
|
const toggleSelectAll = (checked: boolean) => {
|
|||
|
|
if (checked) {
|
|||
|
|
selectedResources.value = resources.value.map(r => r.id)
|
|||
|
|
} else {
|
|||
|
|
selectedResources.value = []
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const toggleResourceSelection = (id: number, checked: boolean) => {
|
|||
|
|
if (checked) {
|
|||
|
|
if (!selectedResources.value.includes(id)) {
|
|||
|
|
selectedResources.value.push(id)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
const index = selectedResources.value.indexOf(id)
|
|||
|
|
if (index > -1) {
|
|||
|
|
selectedResources.value.splice(index, 1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 单个转存
|
|||
|
|
const handleSingleTransfer = async (resource: any) => {
|
|||
|
|
resource.transferring = true
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 这里应该调用实际的转存API
|
|||
|
|
// 由于只是UI展示,模拟转存过程
|
|||
|
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
|||
|
|
|
|||
|
|
// 模拟随机成功/失败
|
|||
|
|
const isSuccess = Math.random() > 0.3
|
|||
|
|
|
|||
|
|
if (isSuccess) {
|
|||
|
|
$message.success(`${resource.title} 转存成功`)
|
|||
|
|
// 刷新列表
|
|||
|
|
refreshData()
|
|||
|
|
} else {
|
|||
|
|
$message.error(`${resource.title} 转存失败`)
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('转存失败:', error)
|
|||
|
|
$message.error('转存失败')
|
|||
|
|
} finally {
|
|||
|
|
resource.transferring = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 批量转存
|
|||
|
|
const handleBatchTransfer = async () => {
|
|||
|
|
if (selectedResources.value.length === 0) {
|
|||
|
|
$message.warning('请选择要转存的资源')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
batchTransferring.value = true
|
|||
|
|
transferResults.value = []
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const selectedItems = resources.value.filter(r => selectedResources.value.includes(r.id))
|
|||
|
|
|
|||
|
|
// 这里应该调用实际的批量转存API
|
|||
|
|
// 由于只是UI展示,模拟批量转存过程
|
|||
|
|
for (const item of selectedItems) {
|
|||
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|||
|
|
|
|||
|
|
const isSuccess = Math.random() > 0.3
|
|||
|
|
transferResults.value.push({
|
|||
|
|
id: item.id,
|
|||
|
|
title: item.title,
|
|||
|
|
url: item.url,
|
|||
|
|
success: isSuccess,
|
|||
|
|
message: isSuccess ? '转存成功' : '转存失败:网络错误'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showTransferResult.value = true
|
|||
|
|
|
|||
|
|
// 刷新列表
|
|||
|
|
refreshData()
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('批量转存失败:', error)
|
|||
|
|
$message.error('批量转存失败')
|
|||
|
|
} finally {
|
|||
|
|
batchTransferring.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查看资源详情
|
|||
|
|
const viewResource = (resource: any) => {
|
|||
|
|
console.log('查看资源详情:', resource)
|
|||
|
|
// 这里可以打开资源详情模态框
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取状态类型
|
|||
|
|
const getStatusType = (resource: any) => {
|
|||
|
|
if (resource.is_valid === false) return 'error'
|
|||
|
|
if (resource.is_valid === true) return 'success'
|
|||
|
|
return 'warning'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取状态文本
|
|||
|
|
const getStatusText = (resource: any) => {
|
|||
|
|
if (resource.is_valid === false) return '无效'
|
|||
|
|
if (resource.is_valid === true) return '有效'
|
|||
|
|
return '待验证'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化日期
|
|||
|
|
const formatDate = (dateString: string) => {
|
|||
|
|
return new Date(dateString).toLocaleDateString()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化
|
|||
|
|
onMounted(() => {
|
|||
|
|
fetchCategories()
|
|||
|
|
fetchTags()
|
|||
|
|
fetchUntransferredResources()
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.line-clamp-1 {
|
|||
|
|
display: -webkit-box;
|
|||
|
|
-webkit-line-clamp: 1;
|
|||
|
|
-webkit-box-orient: vertical;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.line-clamp-2 {
|
|||
|
|
display: -webkit-box;
|
|||
|
|
-webkit-line-clamp: 2;
|
|||
|
|
-webkit-box-orient: vertical;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
</style>
|