|
|
@@ -1,11 +1,18 @@
|
|
|
<script setup>
|
|
|
-import { createRecord, deleteRecord, getRecordById, listRecords, updateRecord, uploadFile, downloadFile } from '@/services/api'
|
|
|
+import {
|
|
|
+ createRecord,
|
|
|
+ deleteRecord,
|
|
|
+ getRecordById,
|
|
|
+ listRecords,
|
|
|
+ updateRecord,
|
|
|
+ uploadFile,
|
|
|
+ downloadFile
|
|
|
+} from '@/services/api'
|
|
|
import { Form } from '@primevue/forms'
|
|
|
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
|
|
import { useDateFormat } from '@vueuse/core'
|
|
|
import Button from 'primevue/button'
|
|
|
import Column from 'primevue/column'
|
|
|
-import ConfirmDialog from 'primevue/confirmdialog'
|
|
|
import DataTable from 'primevue/datatable'
|
|
|
import Dialog from 'primevue/dialog'
|
|
|
import FloatLabel from 'primevue/floatlabel'
|
|
|
@@ -69,10 +76,10 @@ const displayDescription = computed({
|
|
|
set(value) {
|
|
|
// 如果用户输入的是格式化后的文本,尝试转换回JSON格式
|
|
|
try {
|
|
|
- const lines = value.split('\n').filter(line => line.trim())
|
|
|
+ const lines = value.split('\n').filter((line) => line.trim())
|
|
|
const obj = {}
|
|
|
let isFormatted = true
|
|
|
-
|
|
|
+
|
|
|
for (const line of lines) {
|
|
|
const colonIndex = line.indexOf(':')
|
|
|
if (colonIndex === -1) {
|
|
|
@@ -83,7 +90,7 @@ const displayDescription = computed({
|
|
|
const val = line.substring(colonIndex + 1).trim()
|
|
|
obj[key] = val
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if (isFormatted && Object.keys(obj).length > 0) {
|
|
|
// 如果看起来像是格式化后的JSON,转换为JSON字符串
|
|
|
recordForm.value.description = JSON.stringify(obj, null, 2)
|
|
|
@@ -133,23 +140,21 @@ const formatDate = (date) => {
|
|
|
// 格式化描述内容
|
|
|
function formatDescription(description) {
|
|
|
if (!description) return ''
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
// 尝试解析JSON
|
|
|
const parsed = JSON.parse(description)
|
|
|
if (typeof parsed === 'object' && parsed !== null) {
|
|
|
- // 如果是JSON对象,按行展示
|
|
|
+ // 如果是JSON对象,按行展示
|
|
|
const formatted = Object.entries(parsed)
|
|
|
.map(([key, value]) => `${key}: ${value}`)
|
|
|
.join('\n')
|
|
|
- console.log('JSON格式化结果:', formatted)
|
|
|
return formatted
|
|
|
}
|
|
|
} catch {
|
|
|
- // 如果不是JSON,返回原文本
|
|
|
- console.log('非JSON文本:', description)
|
|
|
+ // 如果不是JSON,返回原文本
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return description
|
|
|
}
|
|
|
|
|
|
@@ -236,19 +241,32 @@ const saveRecord = async ({ valid, values }) => {
|
|
|
// 删除记录
|
|
|
const handleDeleteRecord = (record) => {
|
|
|
confirm.require({
|
|
|
- message: `确定要删除记录 "${record.description}" 吗?`,
|
|
|
+ message: `确定要删除记录吗?`,
|
|
|
header: '确认删除',
|
|
|
icon: 'pi pi-exclamation-triangle',
|
|
|
+ acceptLabel: '确定',
|
|
|
+ rejectLabel: '取消',
|
|
|
accept: async () => {
|
|
|
try {
|
|
|
await deleteRecord(record.id)
|
|
|
+
|
|
|
toast.add({
|
|
|
severity: 'success',
|
|
|
summary: '成功',
|
|
|
detail: '记录删除成功',
|
|
|
life: 3000
|
|
|
})
|
|
|
- fetchData() // 刷新列表
|
|
|
+
|
|
|
+ // 检查删除后是否需要调整分页
|
|
|
+ const currentPage = tableData.value.metadata.page
|
|
|
+
|
|
|
+ // 如果当前页只有一条记录且不是第一页,删除后跳转到上一页
|
|
|
+ if (tableData.value.content.length === 1 && currentPage > 0) {
|
|
|
+ tableData.value.metadata.page = currentPage - 1
|
|
|
+ }
|
|
|
+
|
|
|
+ // 刷新数据
|
|
|
+ await fetchData()
|
|
|
} catch {
|
|
|
toast.add({
|
|
|
severity: 'error',
|
|
|
@@ -257,6 +275,9 @@ const handleDeleteRecord = (record) => {
|
|
|
life: 3000
|
|
|
})
|
|
|
}
|
|
|
+ },
|
|
|
+ reject: () => {
|
|
|
+ // 用户取消删除,不需要做任何操作
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
@@ -264,12 +285,10 @@ const handleDeleteRecord = (record) => {
|
|
|
// 从URL中提取OSS key
|
|
|
const extractKeyFromUrl = (url) => {
|
|
|
try {
|
|
|
- // 假设URL格式为: https://bucket.oss-region.aliyuncs.com/path/to/file
|
|
|
const urlObj = new URL(url)
|
|
|
// 移除开头的斜杠
|
|
|
return urlObj.pathname.substring(1)
|
|
|
} catch {
|
|
|
- console.error('无法解析URL:', url)
|
|
|
return null
|
|
|
}
|
|
|
}
|
|
|
@@ -277,7 +296,7 @@ const extractKeyFromUrl = (url) => {
|
|
|
// 从描述中提取文件名
|
|
|
const extractFileNameFromDescription = (description) => {
|
|
|
if (!description) return 'download'
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
// 尝试解析JSON
|
|
|
const parsed = JSON.parse(description)
|
|
|
@@ -285,7 +304,7 @@ const extractFileNameFromDescription = (description) => {
|
|
|
// 提取指定字段
|
|
|
const userName = parsed.userName || ''
|
|
|
const userId = parsed.userId || ''
|
|
|
-
|
|
|
+
|
|
|
// 如果两个字段都有值,使用 userId_@userName 格式
|
|
|
if (userId && userName) {
|
|
|
return `${userId}_@${userName}`
|
|
|
@@ -303,7 +322,7 @@ const extractFileNameFromDescription = (description) => {
|
|
|
// 如果不是JSON,返回原描述(去除特殊字符)
|
|
|
return description.replace(/[<>:"/\\|?*]/g, '_').substring(0, 50)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return 'download'
|
|
|
}
|
|
|
|
|
|
@@ -314,13 +333,13 @@ const handleFileDownload = async (url, description) => {
|
|
|
if (!key) {
|
|
|
throw new Error('无法解析文件路径')
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 从描述中提取文件名
|
|
|
const fileName = extractFileNameFromDescription(description)
|
|
|
-
|
|
|
+
|
|
|
// 通过API服务下载文件
|
|
|
const blob = await downloadFile(key)
|
|
|
-
|
|
|
+
|
|
|
const downloadUrl = window.URL.createObjectURL(blob)
|
|
|
const a = document.createElement('a')
|
|
|
a.href = downloadUrl
|
|
|
@@ -329,7 +348,7 @@ const handleFileDownload = async (url, description) => {
|
|
|
a.click()
|
|
|
document.body.removeChild(a)
|
|
|
window.URL.revokeObjectURL(downloadUrl)
|
|
|
-
|
|
|
+
|
|
|
toast.add({
|
|
|
severity: 'success',
|
|
|
summary: '下载成功',
|
|
|
@@ -356,20 +375,16 @@ const handleFileUpload = async (event) => {
|
|
|
const response = await uploadFile(file)
|
|
|
// 根据实际返回的数据结构提取URL
|
|
|
const newUrl = response.data?.url || ''
|
|
|
-
|
|
|
+
|
|
|
// 重新设置整个表单对象来触发验证
|
|
|
recordForm.value = {
|
|
|
...recordForm.value,
|
|
|
url: newUrl
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 更新formKey来强制表单重新渲染
|
|
|
formKey.value++
|
|
|
-
|
|
|
- // 添加调试信息
|
|
|
- console.log('文件上传成功,URL已设置:', newUrl)
|
|
|
- console.log('当前表单值:', recordForm.value)
|
|
|
-
|
|
|
+
|
|
|
toast.add({
|
|
|
severity: 'success',
|
|
|
summary: '上传成功',
|
|
|
@@ -404,34 +419,21 @@ onMounted(() => {
|
|
|
|
|
|
<template>
|
|
|
<div class="rounded-lg p-4 bg-[var(--p-content-background)]">
|
|
|
- <DataTable
|
|
|
- :value="tableData.content"
|
|
|
- :paginator="true"
|
|
|
+ <DataTable :value="tableData.content" :paginator="true"
|
|
|
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
|
|
|
- currentPageReportTemplate="{totalRecords} 条记录 "
|
|
|
- :rows="tableData.metadata.size"
|
|
|
- :rowsPerPageOptions="[10, 20, 50, 100]"
|
|
|
- :totalRecords="tableData.metadata.total"
|
|
|
- @page="handlePageChange"
|
|
|
- lazy
|
|
|
- scrollable
|
|
|
- >
|
|
|
+ currentPageReportTemplate="{totalRecords} 条记录 " :rows="tableData.metadata.size"
|
|
|
+ :rowsPerPageOptions="[10, 20, 50, 100]" :totalRecords="tableData.metadata.total" @page="handlePageChange" lazy
|
|
|
+ scrollable>
|
|
|
<template #header>
|
|
|
<div class="flex flex-wrap items-center">
|
|
|
<Button icon="pi pi-refresh" @click="fetchData" label="刷新" size="small" />
|
|
|
- <Button
|
|
|
- icon="pi pi-plus"
|
|
|
- @click="openNewRecordDialog"
|
|
|
- label="新增记录"
|
|
|
- severity="success"
|
|
|
- size="small"
|
|
|
- class="ml-2"
|
|
|
- />
|
|
|
+ <Button icon="pi pi-plus" @click="openNewRecordDialog" label="新增记录" severity="success" size="small"
|
|
|
+ class="ml-2" />
|
|
|
<div class="flex-1"></div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
- <Column field="id" header="ID" >
|
|
|
+ <Column field="id" header="ID">
|
|
|
<template #body="slotProps">
|
|
|
<span class="font-mono text-sm">{{ slotProps.data.id }}</span>
|
|
|
</template>
|
|
|
@@ -439,11 +441,8 @@ onMounted(() => {
|
|
|
|
|
|
<Column field="description" header="描述" style="min-width: 300px; max-width: 400px">
|
|
|
<template #body="slotProps">
|
|
|
- <div
|
|
|
- class="whitespace-pre-line text-sm"
|
|
|
- :title="formatDescription(slotProps.data.description)"
|
|
|
- style="max-height: 100px; overflow-y: auto;"
|
|
|
- >
|
|
|
+ <div class="whitespace-pre-line text-sm" :title="formatDescription(slotProps.data.description)"
|
|
|
+ style="max-height: 100px; overflow-y: auto">
|
|
|
{{ formatDescription(slotProps.data.description) }}
|
|
|
</div>
|
|
|
</template>
|
|
|
@@ -452,32 +451,12 @@ onMounted(() => {
|
|
|
<Column header="操作" style="width: 400px">
|
|
|
<template #body="slotProps">
|
|
|
<div class="flex gap-1">
|
|
|
- <Button
|
|
|
- icon="pi pi-download"
|
|
|
- size="small"
|
|
|
- text
|
|
|
- rounded
|
|
|
- @click="handleFileDownload(slotProps.data.url, slotProps.data.description)"
|
|
|
- :title="'下载文件'"
|
|
|
- />
|
|
|
- <Button
|
|
|
- icon="pi pi-pencil"
|
|
|
- severity="info"
|
|
|
- size="small"
|
|
|
- text
|
|
|
- rounded
|
|
|
- aria-label="编辑"
|
|
|
- @click="openEditRecordDialog(slotProps.data)"
|
|
|
- />
|
|
|
- <Button
|
|
|
- icon="pi pi-trash"
|
|
|
- severity="danger"
|
|
|
- size="small"
|
|
|
- text
|
|
|
- rounded
|
|
|
- aria-label="删除"
|
|
|
- @click="handleDeleteRecord(slotProps.data)"
|
|
|
- />
|
|
|
+ <Button icon="pi pi-download" size="small" text rounded
|
|
|
+ @click="handleFileDownload(slotProps.data.url, slotProps.data.description)" :title="'下载文件'" />
|
|
|
+ <Button icon="pi pi-pencil" severity="info" size="small" text rounded aria-label="编辑"
|
|
|
+ @click="openEditRecordDialog(slotProps.data)" />
|
|
|
+ <Button icon="pi pi-trash" severity="danger" size="small" text rounded aria-label="删除"
|
|
|
+ @click="handleDeleteRecord(slotProps.data)" />
|
|
|
</div>
|
|
|
</template>
|
|
|
</Column>
|
|
|
@@ -490,36 +469,19 @@ onMounted(() => {
|
|
|
</DataTable>
|
|
|
|
|
|
<!-- 记录表单对话框 -->
|
|
|
- <Dialog
|
|
|
- v-model:visible="recordDialog"
|
|
|
- :modal="true"
|
|
|
- :header="isEditMode ? '编辑记录' : '创建记录'"
|
|
|
- :style="{ width: '450px' }"
|
|
|
- position="center"
|
|
|
- >
|
|
|
- <Form ref="formRef" :key="formKey" v-slot="$form" :resolver="recordFormResolver" :initialValues="recordForm" @submit="saveRecord" class="p-fluid">
|
|
|
+ <Dialog v-model:visible="recordDialog" :modal="true" :header="isEditMode ? '编辑记录' : '创建记录'"
|
|
|
+ :style="{ width: '450px' }" position="center">
|
|
|
+ <Form ref="formRef" :key="formKey" v-slot="$form" :resolver="recordFormResolver" :initialValues="recordForm"
|
|
|
+ @submit="saveRecord" class="p-fluid">
|
|
|
<div class="field mt-4">
|
|
|
<FloatLabel variant="on">
|
|
|
<div class="flex gap-2">
|
|
|
<IconField class="flex-1">
|
|
|
<InputIcon class="pi pi-link" />
|
|
|
- <InputText
|
|
|
- id="url"
|
|
|
- name="url"
|
|
|
- v-model="recordForm.url"
|
|
|
- autocomplete="off"
|
|
|
- fluid
|
|
|
- />
|
|
|
+ <InputText id="url" name="url" v-model="recordForm.url" autocomplete="off" fluid />
|
|
|
</IconField>
|
|
|
- <Button
|
|
|
- icon="pi pi-upload"
|
|
|
- @click="triggerFileSelect"
|
|
|
- :loading="isUploading"
|
|
|
- :disabled="isUploading"
|
|
|
- size="small"
|
|
|
- severity="secondary"
|
|
|
- :title="'上传文件'"
|
|
|
- />
|
|
|
+ <Button icon="pi pi-upload" @click="triggerFileSelect" :loading="isUploading" :disabled="isUploading"
|
|
|
+ size="small" severity="secondary" :title="'上传文件'" />
|
|
|
</div>
|
|
|
<label for="url">URL</label>
|
|
|
</FloatLabel>
|
|
|
@@ -529,25 +491,12 @@ onMounted(() => {
|
|
|
</div>
|
|
|
|
|
|
<!-- 隐藏的文件输入框 -->
|
|
|
- <input
|
|
|
- ref="fileInputRef"
|
|
|
- type="file"
|
|
|
- @change="handleFileUpload"
|
|
|
- style="display: none"
|
|
|
- accept="*/*"
|
|
|
- />
|
|
|
+ <input ref="fileInputRef" type="file" @change="handleFileUpload" style="display: none" accept="*/*" />
|
|
|
|
|
|
<div class="field mt-4">
|
|
|
<FloatLabel variant="on">
|
|
|
- <Textarea
|
|
|
- id="description"
|
|
|
- name="description"
|
|
|
- v-model="displayDescription"
|
|
|
- autocomplete="off"
|
|
|
- fluid
|
|
|
- rows="4"
|
|
|
- autoResize
|
|
|
- />
|
|
|
+ <Textarea id="description" name="description" v-model="displayDescription" autocomplete="off" fluid rows="4"
|
|
|
+ autoResize />
|
|
|
<label for="description">描述</label>
|
|
|
</FloatLabel>
|
|
|
<Message v-if="$form.description?.invalid" severity="error" size="small" variant="simple">
|
|
|
@@ -556,29 +505,21 @@ onMounted(() => {
|
|
|
</div>
|
|
|
|
|
|
<div class="flex justify-end gap-2 mt-4">
|
|
|
- <Button
|
|
|
- label="取消"
|
|
|
- severity="secondary"
|
|
|
- type="button"
|
|
|
- @click="recordDialog = false"
|
|
|
- :disabled="recordFormLoading"
|
|
|
- />
|
|
|
+ <Button label="取消" severity="secondary" type="button" @click="recordDialog = false"
|
|
|
+ :disabled="recordFormLoading" />
|
|
|
<Button label="保存" type="submit" :loading="recordFormLoading" />
|
|
|
</div>
|
|
|
</Form>
|
|
|
</Dialog>
|
|
|
-
|
|
|
- <!-- 确认对话框 -->
|
|
|
- <ConfirmDialog />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
-.p-datatable-sm .p-datatable-tbody > tr > td {
|
|
|
+.p-datatable-sm .p-datatable-tbody>tr>td {
|
|
|
padding: 0.5rem;
|
|
|
}
|
|
|
|
|
|
-.p-datatable-sm .p-datatable-thead > tr > th {
|
|
|
+.p-datatable-sm .p-datatable-thead>tr>th {
|
|
|
padding: 0.5rem;
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|