更新上传接口和文章发布编辑界面
This commit is contained in:
parent
06779d999f
commit
3c0f01eb36
5
frontend/components.d.ts
vendored
5
frontend/components.d.ts
vendored
@ -12,8 +12,6 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
|
|
||||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
|
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||||
@ -23,7 +21,6 @@ declare module 'vue' {
|
|||||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
|
||||||
ElForm: typeof import('element-plus/es')['ElForm']
|
ElForm: typeof import('element-plus/es')['ElForm']
|
||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
@ -36,8 +33,8 @@ declare module 'vue' {
|
|||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
|
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
}
|
}
|
||||||
|
|||||||
1372
frontend/package-lock.json
generated
1372
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@
|
|||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"element-plus": "^2.13.0",
|
"element-plus": "^2.13.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
|
"quill": "^2.0.3",
|
||||||
"unplugin-auto-import": "^20.3.0",
|
"unplugin-auto-import": "^20.3.0",
|
||||||
"unplugin-vue-components": "^30.0.0",
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
|
|||||||
@ -1,5 +1,33 @@
|
|||||||
//进行接口API的统一管理
|
//进行接口API的统一管理
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
export class UserService {
|
export class update {
|
||||||
|
/**
|
||||||
|
* @description 上传图片接口
|
||||||
|
* @param {FormData} formData - 包含图片文件的表单数据
|
||||||
|
* @return {Promise} 返回请求结果
|
||||||
|
*/
|
||||||
|
static async uploadImage(formData: FormData) {
|
||||||
|
// 使用XMLHttpRequest确保正确的multipart/form-data处理
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', 'http://localhost:8000/index/index/update_imgs');
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(xhr.responseText);
|
||||||
|
resolve({ data: response, status: xhr.status, statusText: xhr.statusText });
|
||||||
|
} catch (e) {
|
||||||
|
resolve({ data: xhr.responseText, status: xhr.status, statusText: xhr.statusText });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = () => reject(new Error('Network Error'));
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -10,4 +10,53 @@ export class article {
|
|||||||
static async getArticleDetail(id: string) {
|
static async getArticleDetail(id: string) {
|
||||||
return request("/index/articles/getArticleDetail", { id }, "get");
|
return request("/index/articles/getArticleDetail", { id }, "get");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取用户文章列表
|
||||||
|
* @param {string} uid - 用户ID
|
||||||
|
* @return {Promise} 返回请求结果
|
||||||
|
*/
|
||||||
|
static async getUserArticleList(uid: string) {
|
||||||
|
return request("/index/articles/getUserArticleList", { uid }, "get");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取文章分类
|
||||||
|
* @return {Promise} 返回请求结果
|
||||||
|
*/
|
||||||
|
static async getArticleCategory() {
|
||||||
|
return request("/index/articles/getArticleCategory", {}, "get");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 发布文章
|
||||||
|
* @param {object} articleData - 文章数据
|
||||||
|
* @return {Promise} 返回请求结果
|
||||||
|
*/
|
||||||
|
static async publishArticle(articleData: any) {
|
||||||
|
return request("/index/articles/publishArticle", articleData, "post");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 更新文章
|
||||||
|
* @param {string} id - 文章ID
|
||||||
|
* @param {object} articleData - 文章数据
|
||||||
|
* @return {Promise} 返回请求结果
|
||||||
|
*/
|
||||||
|
static async updateArticle(id: string, articleData: any) {
|
||||||
|
return request(
|
||||||
|
"/index/articles/updateArticle",
|
||||||
|
{ id, ...articleData },
|
||||||
|
"post"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 删除文章
|
||||||
|
* @param {string} id - 文章ID
|
||||||
|
* @return {Promise} 返回请求结果
|
||||||
|
*/
|
||||||
|
static async deleteArticle(id: string) {
|
||||||
|
return request("/index/articles/deleteArticle", { id }, "post");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,11 +3,52 @@ import { request } from "./axios";
|
|||||||
|
|
||||||
export class resource {
|
export class resource {
|
||||||
/**
|
/**
|
||||||
* @description 获取resource文章详情
|
* @description 获取resource资源详情
|
||||||
* @param {string} id - 内容ID
|
* @param {string} id - 内容ID
|
||||||
* @return {Promise} 返回请求结果
|
* @return {Promise} 返回请求结果
|
||||||
*/
|
*/
|
||||||
static async getResourceDetail(id: string) {
|
static async getResourceDetail(id: string) {
|
||||||
return request("/index/resources/getResourceDetail", { id }, "get");
|
return request("/index/resources/getResourceDetail", { id }, "get");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取用户资源列表
|
||||||
|
* @param {string} uid - 用户ID
|
||||||
|
* @return {Promise} 返回请求结果
|
||||||
|
*/
|
||||||
|
static async getUserResourceList(uid: string) {
|
||||||
|
return request("/index/resources/getUserResourceList", { uid }, "get");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 发布资源
|
||||||
|
* @param {object} resourceData - 资源数据
|
||||||
|
* @return {Promise} 返回请求结果
|
||||||
|
*/
|
||||||
|
static async publishResource(resourceData: any) {
|
||||||
|
return request("/index/resources/publishResource", resourceData, "post");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 更新资源
|
||||||
|
* @param {string} id - 资源ID
|
||||||
|
* @param {object} resourceData - 资源数据
|
||||||
|
* @return {Promise} 返回请求结果
|
||||||
|
*/
|
||||||
|
static async updateResource(id: string, resourceData: any) {
|
||||||
|
return request(
|
||||||
|
"/index/resources/updateResource",
|
||||||
|
{ id, ...resourceData },
|
||||||
|
"post"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 删除资源
|
||||||
|
* @param {string} id - 资源ID
|
||||||
|
* @return {Promise} 返回请求结果
|
||||||
|
*/
|
||||||
|
static async deleteResource(id: string) {
|
||||||
|
return request("/index/resources/deleteResource", { id }, "post");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,20 @@
|
|||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
import "@/assets/less/global.less";
|
import "@/assets/less/global.less";
|
||||||
import "@/assets/css/all.css"
|
import "@/assets/css/all.css"
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
import QuillEditor from "@/views/components/QuillEditor.vue"
|
||||||
|
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
createApp(App).use(router).use(pinia).use(ElementPlus).mount("#app");
|
app.use(router).use(pinia).use(ElementPlus)
|
||||||
|
|
||||||
|
// 注册全局组件
|
||||||
|
app.component('QuillEditor', QuillEditor)
|
||||||
|
|
||||||
|
app.mount("#app");
|
||||||
|
|||||||
@ -57,10 +57,73 @@ const router = createRouter({
|
|||||||
name: "login",
|
name: "login",
|
||||||
component: () => import("@/views/login/index.vue"),
|
component: () => import("@/views/login/index.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/test-editor",
|
||||||
|
name: "testEditor",
|
||||||
|
component: () => import("@/views/test-editor.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/user/profile",
|
path: "/user/profile",
|
||||||
name: "userProfile",
|
name: "userProfile",
|
||||||
component: () => import("@/views/user/profile/index.vue"),
|
component: () => import("@/views/user/profile/index.vue"),
|
||||||
|
redirect: "/user/profile/basic", // 默认跳转到基本资料
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "basic",
|
||||||
|
name: "profileBasic",
|
||||||
|
component: () => import("@/views/user/profile/basicInfo.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "wallet",
|
||||||
|
name: "profileWallet",
|
||||||
|
component: () => import("@/views/user/profile/wallet.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "messages",
|
||||||
|
name: "profileMessages",
|
||||||
|
component: () => import("@/views/user/profile/messages.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "security",
|
||||||
|
name: "profileSecurity",
|
||||||
|
component: () => import("@/views/user/profile/security.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "notifications",
|
||||||
|
name: "profileNotifications",
|
||||||
|
component: () => import("@/views/user/profile/notifications.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "article/list",
|
||||||
|
name: "articleList",
|
||||||
|
component: () => import("@/views/user/profile/listArticle.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "article/publish",
|
||||||
|
name: "articlePublish",
|
||||||
|
component: () => import("@/views/user/profile/editArticle.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "article/edit",
|
||||||
|
name: "articleEdit",
|
||||||
|
component: () => import("@/views/user/profile/editArticle.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "resource/list",
|
||||||
|
name: "resourceList",
|
||||||
|
component: () => import("@/views/user/profile/listResource.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "resource/publish",
|
||||||
|
name: "resourcePublish",
|
||||||
|
component: () => import("@/views/user/profile/editResource.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "resource/edit",
|
||||||
|
name: "resourceEdit",
|
||||||
|
component: () => import("@/views/user/profile/editResource.vue"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
172
frontend/src/views/components/QuillEditor.vue
Normal file
172
frontend/src/views/components/QuillEditor.vue
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<div class="quill-editor-wrapper">
|
||||||
|
<div ref="editorContainer"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||||
|
import Quill from 'quill'
|
||||||
|
import 'quill/dist/quill.snow.css'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: string
|
||||||
|
placeholder?: string
|
||||||
|
height?: number
|
||||||
|
readonly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: string): void
|
||||||
|
(e: 'change', value: string): void
|
||||||
|
(e: 'blur'): void
|
||||||
|
(e: 'focus'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: '',
|
||||||
|
placeholder: '请输入内容...',
|
||||||
|
height: 300,
|
||||||
|
readonly: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const editorContainer = ref<HTMLDivElement>()
|
||||||
|
let quill: Quill | null = null
|
||||||
|
|
||||||
|
const initEditor = async () => {
|
||||||
|
if (!editorContainer.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Quill 配置
|
||||||
|
const options = {
|
||||||
|
theme: 'snow',
|
||||||
|
placeholder: props.placeholder,
|
||||||
|
readOnly: props.readonly,
|
||||||
|
modules: {
|
||||||
|
toolbar: [
|
||||||
|
['bold', 'italic', 'underline', 'strike'],
|
||||||
|
['blockquote', 'code-block'],
|
||||||
|
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||||
|
[{ 'script': 'sub'}, { 'script': 'super' }],
|
||||||
|
[{ 'indent': '-1'}, { 'indent': '+1' }],
|
||||||
|
[{ 'size': ['small', false, 'large', 'huge'] }],
|
||||||
|
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
|
||||||
|
[{ 'color': [] }, { 'background': [] }],
|
||||||
|
[{ 'align': [] }],
|
||||||
|
['link', 'image', 'video'],
|
||||||
|
['clean']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 Quill 实例
|
||||||
|
quill = new Quill(editorContainer.value, options)
|
||||||
|
|
||||||
|
// 设置初始内容
|
||||||
|
if (props.modelValue) {
|
||||||
|
quill.root.innerHTML = props.modelValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听内容变化
|
||||||
|
quill.on('text-change', () => {
|
||||||
|
const html = quill?.root.innerHTML || ''
|
||||||
|
emit('update:modelValue', html)
|
||||||
|
emit('change', html)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听焦点事件
|
||||||
|
quill.on('selection-change', (range: any) => {
|
||||||
|
if (range) {
|
||||||
|
emit('focus')
|
||||||
|
} else {
|
||||||
|
emit('blur')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Quill 初始化失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const destroyEditor = () => {
|
||||||
|
if (quill) {
|
||||||
|
// Quill 没有 destroy 方法,但可以清理事件监听器
|
||||||
|
quill = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 modelValue 变化
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (quill && newValue !== quill.root.innerHTML) {
|
||||||
|
quill.root.innerHTML = newValue || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听 readonly 变化
|
||||||
|
watch(() => props.readonly, (newValue) => {
|
||||||
|
if (quill) {
|
||||||
|
quill.enable(!newValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
initEditor()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
destroyEditor()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
getQuill: () => quill,
|
||||||
|
getHtml: () => quill?.root.innerHTML || '',
|
||||||
|
getText: () => quill?.getText() || '',
|
||||||
|
setHtml: (html: string) => {
|
||||||
|
if (quill) {
|
||||||
|
quill.root.innerHTML = html
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focus: () => {
|
||||||
|
if (quill) {
|
||||||
|
quill.focus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
blur: () => {
|
||||||
|
if (quill) {
|
||||||
|
quill.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.quill-editor-wrapper {
|
||||||
|
:deep(.ql-toolbar) {
|
||||||
|
border-top: 1px solid #ccc !important;
|
||||||
|
border-left: 1px solid #ccc !important;
|
||||||
|
border-right: 1px solid #ccc !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ql-container) {
|
||||||
|
border: 1px solid #ccc !important;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
min-height: v-bind('props.height + "px"');
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ql-editor) {
|
||||||
|
min-height: v-bind('props.height - 42 + "px"');
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ql-editor.ql-blank::before) {
|
||||||
|
color: #999;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
137
frontend/src/views/components/README.md
Normal file
137
frontend/src/views/components/README.md
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# QuillEditor 富文本编辑器组件
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
QuillEditor 是一个基于 Vue 3 的富文本编辑器组件,封装了 Quill.js 富文本编辑器库,提供了简单易用的 API。
|
||||||
|
|
||||||
|
## 全局注册
|
||||||
|
|
||||||
|
组件已自动在 `main.ts` 中注册为全局组件,可在任何地方直接使用。
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<quill-editor
|
||||||
|
v-model="content"
|
||||||
|
placeholder="请输入内容..."
|
||||||
|
:height="300"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const content = ref('')
|
||||||
|
|
||||||
|
const handleChange = (html) => {
|
||||||
|
console.log('内容变化:', html)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
| 参数 | 类型 | 默认值 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| modelValue | string | '' | 编辑器内容(双向绑定) |
|
||||||
|
| placeholder | string | '请输入内容...' | 占位符文本 |
|
||||||
|
| height | number | 300 | 编辑器高度(px) |
|
||||||
|
| readonly | boolean | false | 是否只读 |
|
||||||
|
| config | object | {} | 编辑器配置对象 |
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
| 事件名 | 参数 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| update:modelValue | string | 内容变化时触发(v-model) |
|
||||||
|
| change | string | 内容变化时触发 |
|
||||||
|
| blur | - | 失去焦点时触发 |
|
||||||
|
| focus | - | 获得焦点时触发 |
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
组件暴露了以下方法,可以通过 ref 调用:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<quill-editor ref="editorRef" v-model="content" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const editorRef = ref()
|
||||||
|
|
||||||
|
// 获取 HTML 内容
|
||||||
|
const getHtml = () => {
|
||||||
|
return editorRef.value.getHtml()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取纯文本内容
|
||||||
|
const getText = () => {
|
||||||
|
return editorRef.value.getText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 HTML 内容
|
||||||
|
const setContent = () => {
|
||||||
|
editorRef.value.setHtml('<p>新的内容</p>')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取编辑器实例
|
||||||
|
const getEditor = () => {
|
||||||
|
return editorRef.value.getEditor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 聚焦
|
||||||
|
editorRef.value.focus()
|
||||||
|
|
||||||
|
// 失焦
|
||||||
|
editorRef.value.blur()
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
可以通过 `config` prop 传递更多配置:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<quill-editor
|
||||||
|
:config="{
|
||||||
|
placeholder: '自定义占位符',
|
||||||
|
readOnly: false,
|
||||||
|
toolbarConfig: {
|
||||||
|
toolbarKeys: ['bold', 'italic', 'underline', '|', 'color', 'bgColor']
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工具栏功能
|
||||||
|
|
||||||
|
默认工具栏包含完整的格式化功能:
|
||||||
|
- **文本格式:** 加粗、斜体、下划线、删除线、上标、下标
|
||||||
|
- **标题:** 1-6级标题
|
||||||
|
- **列表:** 有序列表、无序列表
|
||||||
|
- **引用和代码:** 引用块、代码块
|
||||||
|
- **颜色:** 文字颜色、背景色
|
||||||
|
- **对齐:** 左对齐、居中、右对齐、两端对齐
|
||||||
|
- **字体大小:** 小、正常、大、巨大
|
||||||
|
- **缩进:** 增加/减少缩进
|
||||||
|
- **多媒体:** 链接、图片、视频
|
||||||
|
- **清除格式:** 清除所有格式
|
||||||
|
|
||||||
|
工具栏支持自定义配置。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 组件会自动处理编辑器的创建和销毁
|
||||||
|
2. 支持 v-model 双向绑定
|
||||||
|
3. 在组件卸载时会自动清理编辑器实例
|
||||||
|
4. 支持响应式高度调整
|
||||||
|
5. 默认包含完整的工具栏功能
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
查看 `src/views/user/profile/editArticle.vue` 中的实际使用示例。
|
||||||
102
frontend/src/views/test-editor.vue
Normal file
102
frontend/src/views/test-editor.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div class="test-editor">
|
||||||
|
<h2>WangEditor 测试页面</h2>
|
||||||
|
|
||||||
|
<div class="editor-section">
|
||||||
|
<h3>富文本编辑器</h3>
|
||||||
|
<quill-editor
|
||||||
|
v-model="content"
|
||||||
|
placeholder="在这里输入内容..."
|
||||||
|
:height="400"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-section">
|
||||||
|
<h3>当前内容 (HTML)</h3>
|
||||||
|
<pre>{{ content }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<el-button @click="setContent">设置内容</el-button>
|
||||||
|
<el-button @click="getHtml">获取HTML</el-button>
|
||||||
|
<el-button @click="getText">获取文本</el-button>
|
||||||
|
<el-button @click="clearContent">清空内容</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const content = ref('<p>这是初始内容</p>')
|
||||||
|
|
||||||
|
const handleChange = (html: string) => {
|
||||||
|
console.log('内容变化:', html)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setContent = () => {
|
||||||
|
content.value = '<p><strong>这是新设置的内容</strong></p><p>支持 <em>多种格式</em></p>'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHtml = () => {
|
||||||
|
ElMessage.info(`HTML长度: ${content.value.length}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getText = () => {
|
||||||
|
const text = content.value.replace(/<[^>]*>/g, '')
|
||||||
|
ElMessage.info(`纯文本: ${text}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearContent = () => {
|
||||||
|
content.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.test-editor {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,569 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="content-section">
|
|
||||||
<div class="content-header">
|
|
||||||
<h2 class="content-title">文章管理</h2>
|
|
||||||
<p class="content-desc">管理您的技术文章</p>
|
|
||||||
</div>
|
|
||||||
<div class="content-body">
|
|
||||||
<!-- 操作栏 -->
|
|
||||||
<div class="action-bar">
|
|
||||||
<el-button type="primary" @click="showPublishDialog = true">
|
|
||||||
<i class="fa-solid fa-plus"></i>
|
|
||||||
发布文章
|
|
||||||
</el-button>
|
|
||||||
<div class="search-box">
|
|
||||||
<el-input
|
|
||||||
v-model="searchText"
|
|
||||||
placeholder="搜索文章..."
|
|
||||||
clearable
|
|
||||||
@input="handleSearch"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<i class="fa-solid fa-search"></i>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文章列表 -->
|
|
||||||
<div class="article-list">
|
|
||||||
<div v-if="filteredArticles.length === 0" class="empty-state">
|
|
||||||
<i class="fa-solid fa-file-alt"></i>
|
|
||||||
<p>{{ searchText ? '未找到相关文章' : '暂无文章' }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="article-grid">
|
|
||||||
<div
|
|
||||||
v-for="article in paginatedArticles"
|
|
||||||
:key="article.id"
|
|
||||||
class="article-card"
|
|
||||||
>
|
|
||||||
<div class="article-content-area">
|
|
||||||
<div class="article-header">
|
|
||||||
<el-tag :type="getStatusType(article.status)" size="small">
|
|
||||||
{{ getStatusText(article.status) }}
|
|
||||||
</el-tag>
|
|
||||||
<h3 class="article-title">{{ article.title }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="article-meta">
|
|
||||||
<span class="article-category">{{ getCategoryText(article.category) }}</span>
|
|
||||||
<span class="article-date">{{ article.createTime }}</span>
|
|
||||||
<span class="article-views">{{ article.views }} 阅读</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="article-content">
|
|
||||||
{{ article.content.substring(0, 100) }}...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="article-actions">
|
|
||||||
<el-button size="small" @click="editArticle(article)">编辑</el-button>
|
|
||||||
<el-button size="small" type="danger" @click="deleteArticle(article)">删除</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分页组件 -->
|
|
||||||
<div v-if="filteredArticles.length > 0" class="pagination-wrapper">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="currentPage"
|
|
||||||
v-model:page-size="pageSize"
|
|
||||||
:page-sizes="[5, 10, 20, 50]"
|
|
||||||
:total="totalArticles"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@current-change="handlePageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 发布/编辑文章弹窗 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="showPublishDialog"
|
|
||||||
:title="isEditMode ? '编辑文章' : '发布文章'"
|
|
||||||
width="800px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
>
|
|
||||||
<el-form :model="articleForm" label-width="80px" :rules="formRules" ref="formRef">
|
|
||||||
<el-form-item label="标题" prop="title" required>
|
|
||||||
<el-input
|
|
||||||
v-model="articleForm.title"
|
|
||||||
placeholder="请输入文章标题"
|
|
||||||
maxlength="100"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="分类" prop="category" required>
|
|
||||||
<el-select v-model="articleForm.category" placeholder="请选择分类">
|
|
||||||
<el-option
|
|
||||||
v-for="category in categories"
|
|
||||||
:key="category.value"
|
|
||||||
:label="category.label"
|
|
||||||
:value="category.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="标签">
|
|
||||||
<el-input
|
|
||||||
v-model="articleForm.tags"
|
|
||||||
placeholder="请输入标签,用逗号分隔"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="内容" prop="content" required>
|
|
||||||
<el-input
|
|
||||||
v-model="articleForm.content"
|
|
||||||
type="textarea"
|
|
||||||
:rows="12"
|
|
||||||
placeholder="请输入文章内容..."
|
|
||||||
maxlength="10000"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showPublishDialog = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitArticle" :loading="loading">
|
|
||||||
{{ isEditMode ? '保存修改' : '发布文章' }}
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, computed, onMounted } from "vue";
|
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
|
||||||
|
|
||||||
interface ArticleForm {
|
|
||||||
title: string;
|
|
||||||
category: string;
|
|
||||||
tags: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Article {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
category: string;
|
|
||||||
tags: string;
|
|
||||||
status: 'draft' | 'published' | 'hidden';
|
|
||||||
createTime: string;
|
|
||||||
updateTime: string;
|
|
||||||
views: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Category {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const showPublishDialog = ref(false);
|
|
||||||
const isEditMode = ref(false);
|
|
||||||
const searchText = ref('');
|
|
||||||
const loading = ref(false);
|
|
||||||
const formRef = ref();
|
|
||||||
|
|
||||||
// 分页相关
|
|
||||||
const currentPage = ref(1);
|
|
||||||
const pageSize = ref(10);
|
|
||||||
const totalArticles = computed(() => filteredArticles.value.length);
|
|
||||||
|
|
||||||
// 表单数据
|
|
||||||
const articleForm = ref<ArticleForm>({
|
|
||||||
title: '',
|
|
||||||
category: '',
|
|
||||||
tags: '',
|
|
||||||
content: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// 表单验证规则
|
|
||||||
const formRules = {
|
|
||||||
title: [
|
|
||||||
{ required: true, message: '请输入文章标题', trigger: 'blur' },
|
|
||||||
{ min: 1, max: 100, message: '标题长度在 1 到 100 个字符', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
category: [
|
|
||||||
{ required: true, message: '请选择文章分类', trigger: 'change' }
|
|
||||||
],
|
|
||||||
content: [
|
|
||||||
{ required: true, message: '请输入文章内容', trigger: 'blur' },
|
|
||||||
{ min: 10, message: '内容不能少于10个字符', trigger: 'blur' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// 文章列表
|
|
||||||
const articles = ref<Article[]>([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: 'Vue 3 响应式原理详解',
|
|
||||||
content: 'Vue 3 的响应式系统基于 Proxy 实现,与 Vue 2 的 Object.defineProperty 相比具有更好的性能和更完整的响应式覆盖...',
|
|
||||||
category: 'frontend',
|
|
||||||
tags: 'Vue,响应式,Proxy',
|
|
||||||
status: 'published',
|
|
||||||
createTime: '2024-01-15',
|
|
||||||
updateTime: '2024-01-15',
|
|
||||||
views: 1250
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
title: 'Go 语言高并发编程实践',
|
|
||||||
content: '在 Go 语言中,高并发编程是其核心特性之一。通过 goroutine 和 channel,我们可以轻松实现并发编程...',
|
|
||||||
category: 'backend',
|
|
||||||
tags: 'Go,并发,goroutine',
|
|
||||||
status: 'published',
|
|
||||||
createTime: '2024-01-10',
|
|
||||||
updateTime: '2024-01-12',
|
|
||||||
views: 890
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 分类选项
|
|
||||||
const categories = ref<Category[]>([
|
|
||||||
{ value: 'frontend', label: '前端开发' },
|
|
||||||
{ value: 'backend', label: '后端开发' },
|
|
||||||
{ value: 'mobile', label: '移动开发' },
|
|
||||||
{ value: 'ai', label: '人工智能' },
|
|
||||||
{ value: 'devops', label: 'DevOps' },
|
|
||||||
{ value: 'other', label: '其他' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 计算属性:过滤后的文章列表
|
|
||||||
const filteredArticles = computed(() => {
|
|
||||||
let filtered = articles.value;
|
|
||||||
|
|
||||||
// 搜索过滤
|
|
||||||
if (searchText.value) {
|
|
||||||
filtered = filtered.filter(article =>
|
|
||||||
article.title.toLowerCase().includes(searchText.value.toLowerCase()) ||
|
|
||||||
article.content.toLowerCase().includes(searchText.value.toLowerCase()) ||
|
|
||||||
article.tags.toLowerCase().includes(searchText.value.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算属性:分页后的文章列表
|
|
||||||
const paginatedArticles = computed(() => {
|
|
||||||
const start = (currentPage.value - 1) * pageSize.value;
|
|
||||||
const end = start + pageSize.value;
|
|
||||||
return filteredArticles.value.slice(start, end);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 分页变化处理
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
currentPage.value = page;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSizeChange = (size: number) => {
|
|
||||||
pageSize.value = size;
|
|
||||||
currentPage.value = 1; // 重置到第一页
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取分类文本
|
|
||||||
const getCategoryText = (category: string) => {
|
|
||||||
const cat = categories.value.find(c => c.value === category);
|
|
||||||
return cat ? cat.label : category;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取状态文本
|
|
||||||
const getStatusText = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'draft': return '草稿';
|
|
||||||
case 'published': return '已发布';
|
|
||||||
case 'hidden': return '隐藏';
|
|
||||||
default: return status;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取状态类型
|
|
||||||
const getStatusType = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'draft': return '';
|
|
||||||
case 'published': return 'success';
|
|
||||||
case 'hidden': return 'warning';
|
|
||||||
default: return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 搜索处理
|
|
||||||
const handleSearch = () => {
|
|
||||||
// 搜索逻辑通过计算属性实现
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// 编辑文章
|
|
||||||
const editArticle = (article: Article) => {
|
|
||||||
isEditMode.value = true;
|
|
||||||
articleForm.value = {
|
|
||||||
title: article.title,
|
|
||||||
category: article.category,
|
|
||||||
tags: article.tags,
|
|
||||||
content: article.content
|
|
||||||
};
|
|
||||||
showPublishDialog.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 查看文章
|
|
||||||
const viewArticle = () => {
|
|
||||||
ElMessage.info('查看文章功能开发中...');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除文章
|
|
||||||
const deleteArticle = async (article: Article) => {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
`确定要删除文章"${article.title}"吗?此操作不可恢复。`,
|
|
||||||
'确认删除',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定删除',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 这里可以调用删除API
|
|
||||||
const index = articles.value.findIndex(a => a.id === article.id);
|
|
||||||
if (index > -1) {
|
|
||||||
articles.value.splice(index, 1);
|
|
||||||
// 检查是否需要调整当前页码
|
|
||||||
const totalPages = Math.ceil(articles.value.length / pageSize.value);
|
|
||||||
if (currentPage.value > totalPages && totalPages > 0) {
|
|
||||||
currentPage.value = totalPages;
|
|
||||||
}
|
|
||||||
ElMessage.success('文章删除成功');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 用户取消删除
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提交文章(发布或更新)
|
|
||||||
const submitArticle = async () => {
|
|
||||||
if (!formRef.value) return;
|
|
||||||
|
|
||||||
await formRef.value.validate(async (valid: boolean) => {
|
|
||||||
if (!valid) return;
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isEditMode.value) {
|
|
||||||
// 更新文章
|
|
||||||
ElMessage.success('文章更新成功');
|
|
||||||
} else {
|
|
||||||
// 发布新文章
|
|
||||||
const newArticle: Article = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
title: articleForm.value.title,
|
|
||||||
content: articleForm.value.content,
|
|
||||||
category: articleForm.value.category,
|
|
||||||
tags: articleForm.value.tags,
|
|
||||||
status: 'published',
|
|
||||||
createTime: new Date().toLocaleDateString(),
|
|
||||||
updateTime: new Date().toLocaleDateString(),
|
|
||||||
views: 0
|
|
||||||
};
|
|
||||||
articles.value.unshift(newArticle);
|
|
||||||
currentPage.value = 1; // 重置到第一页
|
|
||||||
ElMessage.success('文章发布成功');
|
|
||||||
}
|
|
||||||
|
|
||||||
showPublishDialog.value = false;
|
|
||||||
resetForm();
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('操作失败,请重试');
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
const resetForm = () => {
|
|
||||||
articleForm.value = {
|
|
||||||
title: '',
|
|
||||||
category: '',
|
|
||||||
tags: '',
|
|
||||||
content: ''
|
|
||||||
};
|
|
||||||
if (formRef.value) {
|
|
||||||
formRef.value.clearValidate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化数据
|
|
||||||
const initArticles = () => {
|
|
||||||
// 这里可以调用API获取文章列表
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initArticles();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.action-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
.search-box {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-list {
|
|
||||||
.article-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-card {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e4e7ed;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #1677ff;
|
|
||||||
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-content-area {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0; // 防止内容溢出
|
|
||||||
|
|
||||||
.article-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
.article-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
margin: 0;
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
|
|
||||||
.article-category {
|
|
||||||
color: #1677ff;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-content {
|
|
||||||
color: #606266;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: 0;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
line-clamp: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.el-button {
|
|
||||||
width: 80px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
color: #999;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 32px;
|
|
||||||
padding: 20px 0;
|
|
||||||
|
|
||||||
.el-pagination {
|
|
||||||
::v-deep(.el-pager li) {
|
|
||||||
min-width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
line-height: 36px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep(.el-pager li.is-active) {
|
|
||||||
background-color: #1677ff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep(.btn-prev, .btn-next) {
|
|
||||||
min-width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 弹窗表单样式 */
|
|
||||||
.el-dialog {
|
|
||||||
::v-deep(.el-dialog__body) {
|
|
||||||
padding: 20px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-form {
|
|
||||||
.el-form-item {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-textarea {
|
|
||||||
::v-deep(.el-textarea__inner) {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
470
frontend/src/views/user/profile/editArticle.vue
Normal file
470
frontend/src/views/user/profile/editArticle.vue
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-section">
|
||||||
|
<div class="content-header">
|
||||||
|
<h2 class="content-title">{{ isEditMode ? "编辑文章" : "发布文章" }}</h2>
|
||||||
|
<p class="content-desc">
|
||||||
|
{{ isEditMode ? "修改您的文章内容" : "分享您的技术见解" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="content-body">
|
||||||
|
<el-form
|
||||||
|
:model="articleForm"
|
||||||
|
label-width="80px"
|
||||||
|
:rules="formRules"
|
||||||
|
ref="formRef"
|
||||||
|
>
|
||||||
|
<el-form-item label="标题" prop="title" required>
|
||||||
|
<el-input
|
||||||
|
v-model="articleForm.title"
|
||||||
|
placeholder="请输入文章标题"
|
||||||
|
maxlength="100"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="分类" prop="category" required>
|
||||||
|
<el-tree-select
|
||||||
|
v-model="articleForm.category"
|
||||||
|
placeholder="请选择分类"
|
||||||
|
:data="categories"
|
||||||
|
:props="{ label: 'name', value: 'id', children: 'children' }"
|
||||||
|
:render-after-expand="false"
|
||||||
|
filterable
|
||||||
|
check-strictly
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="作者" required>
|
||||||
|
<el-input v-model="articleForm.author" placeholder="请输入作者" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="标签">
|
||||||
|
<el-input
|
||||||
|
v-model="articleForm.tags"
|
||||||
|
placeholder="请输入标签,用逗号分隔"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="是否转载" required>
|
||||||
|
<el-radio-group v-model="articleForm.is_trans" size="default">
|
||||||
|
<el-radio-button label="是" value="1"></el-radio-button>
|
||||||
|
<el-radio-button label="否" value="0"></el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item
|
||||||
|
v-if="articleForm.is_trans === '1'"
|
||||||
|
label="转载链接"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<el-input v-model="articleForm.transurl" placeholder="请输入链接" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
v-model="articleForm.desc"
|
||||||
|
placeholder="请输入描述"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="封面图片">
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
list-type="picture-card"
|
||||||
|
:limit="1"
|
||||||
|
:auto-upload="false"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-remove="handleUploadRemove"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
请上传封面图片,建议尺寸 800x400px
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="内容" prop="content" required>
|
||||||
|
<quill-editor
|
||||||
|
v-model="articleForm.content"
|
||||||
|
placeholder="请输入文章内容..."
|
||||||
|
:height="400"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button @click="goBack">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitArticle" :loading="loading">
|
||||||
|
{{ isEditMode ? "保存修改" : "发布文章" }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { Plus } from "@element-plus/icons-vue";
|
||||||
|
import { article } from "@/api/article";
|
||||||
|
import { update } from "@/api/api";
|
||||||
|
|
||||||
|
interface ArticleForm {
|
||||||
|
title: string;
|
||||||
|
category: number | undefined;
|
||||||
|
tags: string;
|
||||||
|
author: string;
|
||||||
|
content: string;
|
||||||
|
desc: string;
|
||||||
|
is_trans: string;
|
||||||
|
transurl: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Category {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const isEditMode = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const formRef = ref();
|
||||||
|
const fileList = ref<any[]>([]);
|
||||||
|
const selectedFile = ref<File | null>(null);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const articleForm = ref<ArticleForm>({
|
||||||
|
title: "",
|
||||||
|
category: undefined as number | undefined,
|
||||||
|
tags: "",
|
||||||
|
desc: "",
|
||||||
|
author: "",
|
||||||
|
content: "",
|
||||||
|
is_trans: "0",
|
||||||
|
transurl: "",
|
||||||
|
image: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules = {
|
||||||
|
title: [
|
||||||
|
{ required: true, message: "请输入文章标题", trigger: "blur" },
|
||||||
|
{
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
message: "标题长度在 1 到 100 个字符",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
category: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请选择文章分类",
|
||||||
|
trigger: "change",
|
||||||
|
validator: (rule: any, value: any, callback: any) => {
|
||||||
|
if (value === undefined || value === null || value === 0) {
|
||||||
|
callback(new Error("请选择文章分类"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
content: [
|
||||||
|
{ required: true, message: "请输入文章内容", trigger: "blur" },
|
||||||
|
{ min: 10, message: "内容不能少于10个字符", trigger: "blur" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分类选项
|
||||||
|
const categories = ref<Category[]>([]);
|
||||||
|
|
||||||
|
// 获取文章分类
|
||||||
|
const getArticleCategories = async () => {
|
||||||
|
const response: any = await article.getArticleCategory();
|
||||||
|
categories.value = response.data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
const getUserId = () => {
|
||||||
|
const userStr = localStorage.getItem("user");
|
||||||
|
if (userStr) {
|
||||||
|
try {
|
||||||
|
const user = JSON.parse(userStr);
|
||||||
|
return user.uid;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("解析用户信息失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
const initData = async () => {
|
||||||
|
// 获取文章分类
|
||||||
|
await getArticleCategories();
|
||||||
|
|
||||||
|
// 检查是否为编辑模式
|
||||||
|
const articleId = route.query.id as string;
|
||||||
|
if (articleId) {
|
||||||
|
isEditMode.value = true;
|
||||||
|
loadArticle(articleId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载文章数据(编辑模式)
|
||||||
|
const loadArticle = async (articleId: string) => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await article.getArticleDetail(articleId);
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
const articleData = response.data.data.article; // 修正数据路径
|
||||||
|
articleForm.value = {
|
||||||
|
title: articleData.title || "",
|
||||||
|
category: articleData.cate || undefined,
|
||||||
|
tags: articleData.tags || "",
|
||||||
|
desc: articleData.desc || "",
|
||||||
|
author: articleData.author || "",
|
||||||
|
content: articleData.content || "",
|
||||||
|
is_trans: articleData.is_trans || "0",
|
||||||
|
transurl: articleData.transurl || "",
|
||||||
|
image: articleData.image || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果有封面图片,设置fileList
|
||||||
|
if (articleData.image) {
|
||||||
|
// 拼接完整的图片URL
|
||||||
|
const fullImageUrl = articleData.image.startsWith('http')
|
||||||
|
? articleData.image
|
||||||
|
: `http://localhost:8000${articleData.image}`;
|
||||||
|
|
||||||
|
fileList.value = [
|
||||||
|
{
|
||||||
|
name: "cover.jpg",
|
||||||
|
url: fullImageUrl,
|
||||||
|
uid: Date.now(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || "加载文章失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载文章失败:", error);
|
||||||
|
ElMessage.error("加载文章失败,请重试");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传处理方法
|
||||||
|
const handleFileChange = (file: any, fileList: any[]) => {
|
||||||
|
// 存储选择的文件,用于后续上传
|
||||||
|
selectedFile.value = file.raw;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadCoverImage = async (): Promise<string | null> => {
|
||||||
|
if (!selectedFile.value) {
|
||||||
|
return articleForm.value.image; // 如果没有新选择文件,返回原有图片URL
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// 只发送文件字段,测试是否能上传成功
|
||||||
|
formData.append('file', selectedFile.value, selectedFile.value.name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await update.uploadImage(formData);
|
||||||
|
|
||||||
|
// 检查响应状态
|
||||||
|
if (response.status === 200) {
|
||||||
|
const responseData = response.data;
|
||||||
|
|
||||||
|
if (responseData.code === 0) {
|
||||||
|
// 根据后端返回格式:data.url
|
||||||
|
const imageUrl = responseData.data?.url;
|
||||||
|
if (imageUrl) {
|
||||||
|
ElMessage.success("封面上传成功");
|
||||||
|
return imageUrl;
|
||||||
|
} else {
|
||||||
|
console.error('无法获取图片URL:', responseData);
|
||||||
|
ElMessage.error("封面上传失败:无法获取图片URL");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(responseData.msg || "封面上传失败");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error("封面上传失败:服务器响应异常");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('上传错误:', error);
|
||||||
|
const errorMessage = error.response?.data?.msg || error.message || "封面上传失败";
|
||||||
|
ElMessage.error(errorMessage);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadRemove = (file: any, fileList: any[]) => {
|
||||||
|
selectedFile.value = null;
|
||||||
|
articleForm.value.image = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeUpload = (file: any) => {
|
||||||
|
const isImage = file.type.startsWith("image/");
|
||||||
|
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||||
|
|
||||||
|
if (!isImage) {
|
||||||
|
ElMessage.error("只能上传图片文件!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isLt5M) {
|
||||||
|
ElMessage.error("上传图片大小不能超过 5MB!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交文章(发布或更新)
|
||||||
|
const submitArticle = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
|
||||||
|
const userId = getUserId();
|
||||||
|
if (!userId) {
|
||||||
|
ElMessage.warning("请先登录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await formRef.value.validate(async (valid: boolean) => {
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先上传封面图片(如果有选择新文件)
|
||||||
|
const coverImageUrl = await uploadCoverImage();
|
||||||
|
if (coverImageUrl === null && selectedFile.value) {
|
||||||
|
// 上传失败且有选择新文件
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const articleData = {
|
||||||
|
title: articleForm.value.title,
|
||||||
|
content: articleForm.value.content,
|
||||||
|
cate: articleForm.value.category || 0,
|
||||||
|
user_id: userId,
|
||||||
|
desc: articleForm.value.desc,
|
||||||
|
author: articleForm.value.author,
|
||||||
|
is_trans: articleForm.value.is_trans,
|
||||||
|
transurl:
|
||||||
|
articleForm.value.is_trans === "1" ? articleForm.value.transurl : "",
|
||||||
|
image: coverImageUrl || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditMode.value) {
|
||||||
|
// 更新文章
|
||||||
|
const articleId = route.query.id as string;
|
||||||
|
const updateData = {
|
||||||
|
...articleData,
|
||||||
|
id: articleId,
|
||||||
|
};
|
||||||
|
const response = await article.updateArticle(articleId, updateData);
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success("文章更新成功");
|
||||||
|
router.push("/user/profile");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || "文章更新失败");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 发布新文章
|
||||||
|
const response = await article.publishArticle(articleData);
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success("文章发布成功");
|
||||||
|
router.push("/user/profile");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || "文章发布失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("操作失败:", error);
|
||||||
|
ElMessage.error("操作失败,请重试");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.content-section {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.content-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-desc {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-body {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form {
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-textarea {
|
||||||
|
::v-deep(.el-textarea__inner) {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
364
frontend/src/views/user/profile/editResource.vue
Normal file
364
frontend/src/views/user/profile/editResource.vue
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-section">
|
||||||
|
<div class="content-header">
|
||||||
|
<h2 class="content-title">{{ isEditMode ? "编辑资源" : "发布资源" }}</h2>
|
||||||
|
<p class="content-desc">
|
||||||
|
{{ isEditMode ? "修改您的资源信息" : "分享您的应用资源" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="content-body">
|
||||||
|
<el-form
|
||||||
|
:model="resourceForm"
|
||||||
|
label-width="80px"
|
||||||
|
:rules="formRules"
|
||||||
|
ref="formRef"
|
||||||
|
>
|
||||||
|
<el-form-item label="资源名称" prop="name" required>
|
||||||
|
<el-input
|
||||||
|
v-model="resourceForm.name"
|
||||||
|
placeholder="请输入资源名称"
|
||||||
|
maxlength="50"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="资源类型" prop="type" required>
|
||||||
|
<el-select v-model="resourceForm.type" placeholder="请选择资源类型">
|
||||||
|
<el-option
|
||||||
|
v-for="type in resourceTypes"
|
||||||
|
:key="type.value"
|
||||||
|
:label="type.label"
|
||||||
|
:value="type.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="分类" prop="category" required>
|
||||||
|
<el-checkbox-group v-model="resourceForm.category">
|
||||||
|
<el-checkbox
|
||||||
|
v-for="platform in platforms"
|
||||||
|
:key="platform.value"
|
||||||
|
:value="platform.value"
|
||||||
|
>
|
||||||
|
{{ platform.label }}
|
||||||
|
</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="版本号">
|
||||||
|
<el-input v-model="resourceForm.version" placeholder="例如:1.0.0" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="下载链接" prop="downloadUrl" required>
|
||||||
|
<el-input
|
||||||
|
v-model="resourceForm.downloadUrl"
|
||||||
|
placeholder="请输入下载链接"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="资源描述" prop="description" required>
|
||||||
|
<el-input
|
||||||
|
v-model="resourceForm.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入资源描述..."
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="截图上传">
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
:file-list="fileList"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-remove="handleFileRemove"
|
||||||
|
action=""
|
||||||
|
:auto-upload="false"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
list-type="picture-card"
|
||||||
|
>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
<div class="upload-text">上传截图</div>
|
||||||
|
</el-upload>
|
||||||
|
<div class="upload-tips">
|
||||||
|
<p>支持 JPG、PNG、GIF 格式,单张图片不超过 2MB,最多上传 5 张</p>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button @click="goBack">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitResource" :loading="loading">
|
||||||
|
{{ isEditMode ? "保存修改" : "发布资源" }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { Plus } from "@element-plus/icons-vue";
|
||||||
|
import { resource } from "@/api/resource";
|
||||||
|
|
||||||
|
interface ResourceForm {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
category: string[];
|
||||||
|
version: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourceType {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Platform {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const isEditMode = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const formRef = ref();
|
||||||
|
const uploadRef = ref();
|
||||||
|
const fileList = ref([]);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const resourceForm = ref<ResourceForm>({
|
||||||
|
name: "",
|
||||||
|
type: "",
|
||||||
|
category: [],
|
||||||
|
version: "",
|
||||||
|
downloadUrl: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules = {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: "请输入资源名称", trigger: "blur" },
|
||||||
|
{ min: 1, max: 50, message: "名称长度在 1 到 50 个字符", trigger: "blur" },
|
||||||
|
],
|
||||||
|
type: [{ required: true, message: "请选择资源类型", trigger: "change" }],
|
||||||
|
category: [
|
||||||
|
{
|
||||||
|
type: "array",
|
||||||
|
required: true,
|
||||||
|
message: "请至少选择一个分类",
|
||||||
|
trigger: "change",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
downloadUrl: [
|
||||||
|
{ required: true, message: "请输入下载链接", trigger: "blur" },
|
||||||
|
{ type: "url", message: "请输入正确的URL格式", trigger: "blur" },
|
||||||
|
],
|
||||||
|
description: [
|
||||||
|
{ required: true, message: "请输入资源描述", trigger: "blur" },
|
||||||
|
{ min: 10, message: "描述不能少于10个字符", trigger: "blur" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 资源类型选项
|
||||||
|
const resourceTypes = ref<ResourceType[]>([
|
||||||
|
{ value: "software", label: "软件工具" },
|
||||||
|
{ value: "plugin", label: "插件扩展" },
|
||||||
|
{ value: "library", label: "代码库" },
|
||||||
|
{ value: "template", label: "模板素材" },
|
||||||
|
{ value: "other", label: "其他" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 平台选项
|
||||||
|
const platforms = ref<Platform[]>([
|
||||||
|
{ value: "windows", label: "Windows" },
|
||||||
|
{ value: "macos", label: "macOS" },
|
||||||
|
{ value: "linux", label: "Linux" },
|
||||||
|
{ value: "web", label: "Web" },
|
||||||
|
{ value: "mobile", label: "移动端" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
const initData = () => {
|
||||||
|
// 检查是否为编辑模式
|
||||||
|
const resourceId = route.query.id as string;
|
||||||
|
if (resourceId) {
|
||||||
|
isEditMode.value = true;
|
||||||
|
loadResource(resourceId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载资源数据(编辑模式)
|
||||||
|
const loadResource = async (resourceId: string) => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await resource.getResourceDetail(resourceId);
|
||||||
|
if (response.data.code === 0 && response.data.data) {
|
||||||
|
const resourceData = response.data.data;
|
||||||
|
resourceForm.value = {
|
||||||
|
name: resourceData.name || "",
|
||||||
|
type: resourceData.type || "",
|
||||||
|
category: resourceData.category || [],
|
||||||
|
version: resourceData.version || "",
|
||||||
|
downloadUrl: resourceData.download_url || "",
|
||||||
|
description: resourceData.description || "",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || "加载资源失败");
|
||||||
|
router.push("/user/profile/resource/list");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载资源失败:", error);
|
||||||
|
ElMessage.error("加载资源失败,请重试");
|
||||||
|
router.push("/user/profile/resource/list");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文件上传处理
|
||||||
|
const handleFileChange = (file: any, fileList: any[]) => {
|
||||||
|
// 处理文件上传
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileRemove = (file: any, fileList: any[]) => {
|
||||||
|
// 处理文件移除
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交资源(发布或更新)
|
||||||
|
const submitResource = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
|
||||||
|
await formRef.value.validate(async (valid: boolean) => {
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resourceData = {
|
||||||
|
name: resourceForm.value.name,
|
||||||
|
type: resourceForm.value.type,
|
||||||
|
category: resourceForm.value.category,
|
||||||
|
version: resourceForm.value.version || "1.0.0",
|
||||||
|
downloadUrl: resourceForm.value.downloadUrl,
|
||||||
|
description: resourceForm.value.description,
|
||||||
|
// 这里可以添加其他字段,如 user_id 等
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditMode.value) {
|
||||||
|
// 更新资源
|
||||||
|
const resourceId = route.query.id as string;
|
||||||
|
const response = await resource.updateResource(
|
||||||
|
resourceId,
|
||||||
|
resourceData
|
||||||
|
);
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success("资源更新成功");
|
||||||
|
router.push("/user/profile/resource/list");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || "资源更新失败");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 发布新资源
|
||||||
|
const response = await resource.publishResource(resourceData);
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success("资源发布成功");
|
||||||
|
router.push("/user/profile/resource/list");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || "资源发布失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("操作失败:", error);
|
||||||
|
ElMessage.error("操作失败,请重试");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.content-section {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.content-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-desc {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-body {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form {
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.el-checkbox {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-textarea {
|
||||||
|
::v-deep(.el-textarea__inner) {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tips {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -37,26 +37,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="profile-main">
|
<div class="profile-main">
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<!-- 基本资料 -->
|
<!-- 子路由视图 -->
|
||||||
<BasicInfo v-if="activeMenu === 'profile-basic'" />
|
<router-view />
|
||||||
|
|
||||||
<!-- 我的钱包 -->
|
|
||||||
<Wallet v-if="activeMenu === 'profile-wallet'" />
|
|
||||||
|
|
||||||
<!-- 我的消息 -->
|
|
||||||
<Messages v-if="activeMenu === 'profile-messages'" />
|
|
||||||
|
|
||||||
<!-- 安全设置 -->
|
|
||||||
<Security v-if="activeMenu === 'profile-security'" />
|
|
||||||
|
|
||||||
<!-- 发布文章 -->
|
|
||||||
<ArticlePublish v-if="activeMenu === 'article-publish'" />
|
|
||||||
|
|
||||||
<!-- 发布资源 -->
|
|
||||||
<ResourcePublish v-if="activeMenu === 'apps-publish'" />
|
|
||||||
|
|
||||||
<!-- 系统通知 -->
|
|
||||||
<Notifications v-if="activeMenu === 'profile-notifications'" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -69,10 +51,14 @@ import BasicInfo from "@/views/user/profile/basicInfo.vue";
|
|||||||
import Wallet from "@/views/user/profile/wallet.vue";
|
import Wallet from "@/views/user/profile/wallet.vue";
|
||||||
import Messages from "@/views/user/profile/messages.vue";
|
import Messages from "@/views/user/profile/messages.vue";
|
||||||
import Security from "@/views/user/profile/security.vue";
|
import Security from "@/views/user/profile/security.vue";
|
||||||
import ArticlePublish from "@/views/user/profile/articlePublish.vue";
|
import ListArticle from "@/views/user/profile/listArticle.vue";
|
||||||
import ResourcePublish from "@/views/user/profile/resourcePublish.vue";
|
import ListResource from "@/views/user/profile/listResource.vue";
|
||||||
import Notifications from "@/views/user/profile/notifications.vue";
|
import Notifications from "@/views/user/profile/notifications.vue";
|
||||||
import { ref } from "vue";
|
import { ref, watch, onMounted } from "vue";
|
||||||
|
import { useRouter, useRoute } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const activeMenu = ref('profile-basic'); // 当前激活的菜单项
|
const activeMenu = ref('profile-basic'); // 当前激活的菜单项
|
||||||
@ -149,6 +135,23 @@ const menuGroups = ref([
|
|||||||
// 菜单点击事件
|
// 菜单点击事件
|
||||||
const handleMenuClick = (target: string) => {
|
const handleMenuClick = (target: string) => {
|
||||||
activeMenu.value = target;
|
activeMenu.value = target;
|
||||||
|
|
||||||
|
// 根据菜单项跳转到对应的路由
|
||||||
|
if (target === 'profile-basic') {
|
||||||
|
router.push('/user/profile/basic');
|
||||||
|
} else if (target === 'profile-wallet') {
|
||||||
|
router.push('/user/profile/wallet');
|
||||||
|
} else if (target === 'profile-messages') {
|
||||||
|
router.push('/user/profile/messages');
|
||||||
|
} else if (target === 'profile-security') {
|
||||||
|
router.push('/user/profile/security');
|
||||||
|
} else if (target === 'profile-notifications') {
|
||||||
|
router.push('/user/profile/notifications');
|
||||||
|
} else if (target === 'article-publish') {
|
||||||
|
router.push('/user/profile/article/list');
|
||||||
|
} else if (target === 'apps-publish') {
|
||||||
|
router.push('/user/profile/resource/list');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 分组折叠/展开(手风琴效果)
|
// 分组折叠/展开(手风琴效果)
|
||||||
@ -171,8 +174,57 @@ const isGroupCollapsed = (groupName: string) => {
|
|||||||
return collapsedGroups.value.includes(groupName);
|
return collapsedGroups.value.includes(groupName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 根据路由设置活动菜单
|
||||||
|
const setActiveMenuFromRoute = () => {
|
||||||
|
const currentPath = route.path;
|
||||||
|
|
||||||
|
// 重置折叠状态,只展开相关的分组
|
||||||
|
collapsedGroups.value = ['personal', 'apps', 'system']; // 默认都折叠
|
||||||
|
|
||||||
|
if (currentPath.includes('/user/profile/article/publish') ||
|
||||||
|
currentPath.includes('/user/profile/article/edit') ||
|
||||||
|
currentPath.includes('/user/profile/article/list')) {
|
||||||
|
activeMenu.value = 'article-publish';
|
||||||
|
// 只展开 apps 分组
|
||||||
|
collapsedGroups.value = collapsedGroups.value.filter(id => id !== 'apps');
|
||||||
|
} else if (currentPath.includes('/user/profile/resource/publish') ||
|
||||||
|
currentPath.includes('/user/profile/resource/edit') ||
|
||||||
|
currentPath.includes('/user/profile/resource/list')) {
|
||||||
|
activeMenu.value = 'apps-publish';
|
||||||
|
// 只展开 apps 分组
|
||||||
|
collapsedGroups.value = collapsedGroups.value.filter(id => id !== 'apps');
|
||||||
|
} else if (currentPath.includes('/user/profile/basic') ||
|
||||||
|
currentPath.includes('/user/profile/wallet') ||
|
||||||
|
currentPath.includes('/user/profile/messages') ||
|
||||||
|
currentPath.includes('/user/profile/security')) {
|
||||||
|
// 个人中心相关页面
|
||||||
|
if (currentPath.includes('/user/profile/basic')) {
|
||||||
|
activeMenu.value = 'profile-basic';
|
||||||
|
} else if (currentPath.includes('/user/profile/wallet')) {
|
||||||
|
activeMenu.value = 'profile-wallet';
|
||||||
|
} else if (currentPath.includes('/user/profile/messages')) {
|
||||||
|
activeMenu.value = 'profile-messages';
|
||||||
|
} else if (currentPath.includes('/user/profile/security')) {
|
||||||
|
activeMenu.value = 'profile-security';
|
||||||
|
}
|
||||||
|
// 只展开 personal 分组
|
||||||
|
collapsedGroups.value = collapsedGroups.value.filter(id => id !== 'personal');
|
||||||
|
} else if (currentPath.includes('/user/profile/notifications')) {
|
||||||
|
activeMenu.value = 'profile-notifications';
|
||||||
|
// 只展开 system 分组
|
||||||
|
collapsedGroups.value = collapsedGroups.value.filter(id => id !== 'system');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听路由变化,自动设置活动菜单
|
||||||
|
watch(() => route.path, () => {
|
||||||
|
setActiveMenuFromRoute();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件挂载时设置活动菜单
|
||||||
|
onMounted(() => {
|
||||||
|
setActiveMenuFromRoute();
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
|||||||
390
frontend/src/views/user/profile/listArticle.vue
Normal file
390
frontend/src/views/user/profile/listArticle.vue
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-section">
|
||||||
|
<div class="content-header">
|
||||||
|
<h2 class="content-title">文章管理</h2>
|
||||||
|
<p class="content-desc">管理您的技术文章</p>
|
||||||
|
</div>
|
||||||
|
<div class="content-body">
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="goToPublish">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
发布文章
|
||||||
|
</el-button>
|
||||||
|
<div class="search-box">
|
||||||
|
<el-input
|
||||||
|
v-model="searchText"
|
||||||
|
placeholder="搜索文章..."
|
||||||
|
clearable
|
||||||
|
@input="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<i class="fa-solid fa-search"></i>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文章列表 -->
|
||||||
|
<div class="article-list">
|
||||||
|
<div v-if="filteredArticles.length === 0" class="empty-state">
|
||||||
|
<i class="fa-solid fa-file-alt"></i>
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
searchText && articleData.length > 0
|
||||||
|
? "未找到相关文章"
|
||||||
|
: "暂无文章"
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="article-grid">
|
||||||
|
<div
|
||||||
|
v-for="item in paginatedArticles"
|
||||||
|
:key="item.id"
|
||||||
|
class="article-card"
|
||||||
|
>
|
||||||
|
<div class="article-content-area">
|
||||||
|
<div class="article-header">
|
||||||
|
<el-tag type="success" size="small">
|
||||||
|
{{ item.category_name }}
|
||||||
|
</el-tag>
|
||||||
|
<h3 class="article-title">{{ item.title }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="article-meta">
|
||||||
|
<span class="article-author">作者: {{ item.author }}</span>
|
||||||
|
<span class="article-date">
|
||||||
|
{{item.update_time ? item.update_time : item.publish_date || item.publishdate}}
|
||||||
|
</span>
|
||||||
|
<span class="article-views">{{ item.views }} 阅读</span>
|
||||||
|
<span class="article-likes">{{ item.likes }} 点赞</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="article-actions">
|
||||||
|
<el-button size="small" @click="editArticle(item)"
|
||||||
|
>编辑</el-button
|
||||||
|
>
|
||||||
|
<el-button size="small" type="danger" @click="deleteArticle(item)"
|
||||||
|
>删除</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<div v-if="filteredArticles.length > 0" class="pagination-wrapper">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[5, 10, 20, 50]"
|
||||||
|
:total="filteredArticles.length"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
import { article } from "@/api/article";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const articleData = ref<any[]>([]);
|
||||||
|
const searchText = ref("");
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
|
||||||
|
// 计算属性:过滤后的文章列表
|
||||||
|
const filteredArticles = computed(() => {
|
||||||
|
let filtered = articleData.value;
|
||||||
|
|
||||||
|
// 搜索过滤
|
||||||
|
if (searchText.value) {
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(article: any) =>
|
||||||
|
article.title.toLowerCase().includes(searchText.value.toLowerCase()) ||
|
||||||
|
(article.desc || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.value.toLowerCase()) ||
|
||||||
|
(article.category_name || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.value.toLowerCase()) ||
|
||||||
|
(article.author || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.value.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:分页后的文章列表
|
||||||
|
const paginatedArticles = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value;
|
||||||
|
const end = start + pageSize.value;
|
||||||
|
return filteredArticles.value.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页变化处理
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
currentPage.value = page;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
pageSize.value = size;
|
||||||
|
currentPage.value = 1; // 重置到第一页
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
// 搜索逻辑通过计算属性实现
|
||||||
|
};
|
||||||
|
|
||||||
|
// 跳转到发布文章页面
|
||||||
|
const goToPublish = () => {
|
||||||
|
router.push("/user/profile/article/publish");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑文章
|
||||||
|
const editArticle = (article: any) => {
|
||||||
|
router.push({
|
||||||
|
path: "/user/profile/article/edit",
|
||||||
|
query: { id: article.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除文章
|
||||||
|
const deleteArticle = async (article: any) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除文章"${article.title}"吗?此操作不可恢复。`,
|
||||||
|
"确认删除",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定删除",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 调用删除API
|
||||||
|
const response = await article.deleteArticle(article.id);
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success("文章删除成功");
|
||||||
|
await initArticles(); // 重新获取列表
|
||||||
|
|
||||||
|
// 检查是否需要调整当前页码
|
||||||
|
const totalPages = Math.ceil(articleData.value.length / pageSize.value);
|
||||||
|
if (currentPage.value > totalPages && totalPages > 0) {
|
||||||
|
currentPage.value = totalPages;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || "文章删除失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 用户取消删除或API调用失败
|
||||||
|
if (error !== "cancel") {
|
||||||
|
ElMessage.error("文章删除失败,请重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
const getUserId = () => {
|
||||||
|
const userStr = localStorage.getItem("user");
|
||||||
|
if (userStr) {
|
||||||
|
try {
|
||||||
|
const user = JSON.parse(userStr);
|
||||||
|
return user.uid;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("解析用户信息失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
const initArticles = async () => {
|
||||||
|
const uid = getUserId();
|
||||||
|
if (!uid) {
|
||||||
|
ElMessage.warning("请先登录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response: any = await article.getUserArticleList(uid);
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
// 直接使用API返回的数据
|
||||||
|
articleData.value = response.data.data.articles || [];
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || "获取文章列表失败");
|
||||||
|
articleData.value = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取文章列表失败:", error);
|
||||||
|
ElMessage.error("获取文章列表失败,请重试");
|
||||||
|
articleData.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initArticles();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-list {
|
||||||
|
.article-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content-area {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; // 防止内容溢出
|
||||||
|
|
||||||
|
.article-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.article-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
|
||||||
|
.article-category {
|
||||||
|
color: #1677ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content {
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
line-clamp: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 80px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.el-pagination {
|
||||||
|
::v-deep(.el-pager li) {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-pager li.is-active) {
|
||||||
|
background-color: #1677ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.btn-prev, .btn-next) {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
434
frontend/src/views/user/profile/listResource.vue
Normal file
434
frontend/src/views/user/profile/listResource.vue
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-section">
|
||||||
|
<div class="content-header">
|
||||||
|
<h2 class="content-title">资源管理</h2>
|
||||||
|
<p class="content-desc">管理您的应用资源</p>
|
||||||
|
</div>
|
||||||
|
<div class="content-body">
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="goToPublish">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
发布资源
|
||||||
|
</el-button>
|
||||||
|
<div class="filters">
|
||||||
|
<el-input
|
||||||
|
v-model="searchText"
|
||||||
|
placeholder="搜索资源..."
|
||||||
|
clearable
|
||||||
|
@input="handleSearch"
|
||||||
|
style="width: 300px; margin-left: 12px"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<i class="fa-solid fa-search"></i>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 资源列表 -->
|
||||||
|
<div class="resource-list">
|
||||||
|
<div v-if="filteredResources.length === 0" class="empty-state">
|
||||||
|
<i class="fa-solid fa-cubes"></i>
|
||||||
|
<p>
|
||||||
|
{{ searchText || filterType ? "未找到相关资源" : "暂无资源" }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="resource-grid">
|
||||||
|
<div
|
||||||
|
v-for="resource in paginatedResources"
|
||||||
|
:key="resource.id"
|
||||||
|
class="resource-card"
|
||||||
|
>
|
||||||
|
<div class="resource-content-area">
|
||||||
|
<div class="resource-header">
|
||||||
|
<el-tag :type="resource.type" size="small">
|
||||||
|
{{ resource.category_name }}
|
||||||
|
</el-tag>
|
||||||
|
<h3 class="resource-title">
|
||||||
|
{{ resource.title }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="resource-meta">
|
||||||
|
<span class="resource-version"
|
||||||
|
>上传者:{{ resource.uploader }}
|
||||||
|
</span>
|
||||||
|
<span class="resource-date">
|
||||||
|
{{ resource.create_time }}
|
||||||
|
</span>
|
||||||
|
<span class="resource-views">
|
||||||
|
{{ resource.views }} 阅读
|
||||||
|
</span>
|
||||||
|
<span class="resource-downloads">
|
||||||
|
{{ resource.downloads }} 下载
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="resource-description">
|
||||||
|
{{ resource.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="resource-actions">
|
||||||
|
<el-button size="small" @click="editResource(resource)"
|
||||||
|
>编辑</el-button
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
@click="deleteResource(resource)"
|
||||||
|
>删除</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<div v-if="filteredResources.length > 0" class="pagination-wrapper">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[5, 10, 20, 50]"
|
||||||
|
:total="totalResources"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
import { resource } from "@/api/resource";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const resourceData = ref<any[]>([]);
|
||||||
|
const searchText = ref("");
|
||||||
|
const filterType = ref("");
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 分页相关
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
const totalResources = computed(() => filteredResources.value.length);
|
||||||
|
|
||||||
|
// 计算属性:过滤后的资源列表
|
||||||
|
const filteredResources = computed(() => {
|
||||||
|
let filtered = resourceData.value;
|
||||||
|
|
||||||
|
// 搜索过滤
|
||||||
|
if (searchText.value) {
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(resource: any) =>
|
||||||
|
resource.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
|
||||||
|
(resource.description || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.value.toLowerCase()) ||
|
||||||
|
getTypeText(resource.type)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.value.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型过滤
|
||||||
|
if (filterType.value) {
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(resource: any) => resource.type === filterType.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:分页后的资源列表
|
||||||
|
const paginatedResources = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value;
|
||||||
|
const end = start + pageSize.value;
|
||||||
|
return filteredResources.value.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页变化处理
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
currentPage.value = page;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
pageSize.value = size;
|
||||||
|
currentPage.value = 1; // 重置到第一页
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
// 搜索逻辑通过计算属性实现
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取类型文本
|
||||||
|
const getTypeText = (type: string) => {
|
||||||
|
const typeMap: { [key: string]: string } = {
|
||||||
|
android: "Android",
|
||||||
|
ios: "iOS",
|
||||||
|
web: "Web",
|
||||||
|
desktop: "桌面应用",
|
||||||
|
other: "其他",
|
||||||
|
};
|
||||||
|
return typeMap[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取类型颜色
|
||||||
|
const getTypeColor = (type: string): string | undefined => {
|
||||||
|
const colorMap: { [key: string]: string } = {
|
||||||
|
android: "success",
|
||||||
|
ios: "primary",
|
||||||
|
web: "info",
|
||||||
|
desktop: "warning",
|
||||||
|
// 移除'other',让它使用默认样式
|
||||||
|
};
|
||||||
|
return colorMap[type]; // 不设置默认值,让ElTag使用默认样式
|
||||||
|
};
|
||||||
|
|
||||||
|
// 跳转到发布资源页面
|
||||||
|
const goToPublish = () => {
|
||||||
|
router.push("/user/profile/resource/publish");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 跳转到资源列表页面
|
||||||
|
const goToList = () => {
|
||||||
|
router.push("/user/profile/resource/list");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑资源
|
||||||
|
const editResource = (resource: any) => {
|
||||||
|
router.push({
|
||||||
|
path: "/user/profile/resource/edit",
|
||||||
|
query: { id: resource.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除资源
|
||||||
|
const deleteResource = async (resource: any) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除资源"${resource.name}"吗?此操作不可恢复。`,
|
||||||
|
"确认删除",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定删除",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 调用删除API
|
||||||
|
const response = await resource.deleteResource(resource.id);
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success("资源删除成功");
|
||||||
|
await initResources(); // 重新获取列表
|
||||||
|
|
||||||
|
// 检查是否需要调整当前页码
|
||||||
|
const totalPages = Math.ceil(resourceData.value.length / pageSize.value);
|
||||||
|
if (currentPage.value > totalPages && totalPages > 0) {
|
||||||
|
currentPage.value = totalPages;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || "资源删除失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 用户取消删除或API调用失败
|
||||||
|
if (error !== "cancel") {
|
||||||
|
ElMessage.error("资源删除失败,请重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
const getUserId = () => {
|
||||||
|
const userStr = localStorage.getItem("user");
|
||||||
|
if (userStr) {
|
||||||
|
try {
|
||||||
|
const user = JSON.parse(userStr);
|
||||||
|
return user.uid;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("解析用户信息失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
const initResources = async () => {
|
||||||
|
const uid = getUserId();
|
||||||
|
if (!uid) {
|
||||||
|
ElMessage.warning("请先登录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response: any = await resource.getUserResourceList(uid);
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
// 直接使用API返回的数据
|
||||||
|
resourceData.value = response.data.data.resources || [];
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || "获取文章列表失败");
|
||||||
|
resourceData.value = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取文章列表失败:", error);
|
||||||
|
ElMessage.error("获取文章列表失败,请重试");
|
||||||
|
resourceData.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initResources();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-list {
|
||||||
|
.resource-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-content-area {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.resource-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.resource-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-description {
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
line-clamp: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 80px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 32px;
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
.el-pagination {
|
||||||
|
::v-deep(.el-pager li) {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-pager li.is-active) {
|
||||||
|
background-color: #1677ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.btn-prev, .btn-next) {
|
||||||
|
min-width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,49 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="content-section">
|
<div class="content-section">
|
||||||
<div class="content-header">
|
<div class="content-header">
|
||||||
<h2 class="content-title">我的消息</h2>
|
<h2 class="content-title">我的消息</h2>
|
||||||
<p class="content-desc">查看系统消息和通知</p>
|
<p class="content-desc">查看系统消息和通知</p>
|
||||||
</div>
|
|
||||||
<div class="content-body">
|
|
||||||
<div v-if="messages.length === 0" class="empty-state">
|
|
||||||
<i class="fa-solid fa-message"></i>
|
|
||||||
<p>暂无消息</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="message-list">
|
|
||||||
<div
|
|
||||||
v-for="message in messages"
|
|
||||||
:key="message.id"
|
|
||||||
class="message-item"
|
|
||||||
:class="{ 'unread': !message.read }"
|
|
||||||
@click="markAsRead(message.id)"
|
|
||||||
>
|
|
||||||
<div class="message-icon">
|
|
||||||
<i :class="getMessageIcon(message.type)"></i>
|
|
||||||
</div>
|
|
||||||
<div class="message-content">
|
|
||||||
<div class="message-header">
|
|
||||||
<h4 class="message-title">{{ message.title }}</h4>
|
|
||||||
<span class="message-time">{{ message.time }}</span>
|
|
||||||
</div>
|
|
||||||
<p class="message-text">{{ message.content }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="!message.read" class="unread-dot"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="content-body">
|
||||||
|
<div v-if="messages.length === 0" class="empty-state">
|
||||||
|
<i class="fa-solid fa-message"></i>
|
||||||
|
<p>暂无消息</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="message-list">
|
||||||
|
<div
|
||||||
|
v-for="message in messages"
|
||||||
|
:key="message.id"
|
||||||
|
class="message-item"
|
||||||
|
:class="{ unread: !message.read }"
|
||||||
|
@click="markAsRead(message.id)"
|
||||||
|
>
|
||||||
|
<div class="message-icon">
|
||||||
|
<i :class="getMessageIcon(message.type)"></i>
|
||||||
|
</div>
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="message-header">
|
||||||
|
<h4 class="message-title">{{ message.title }}</h4>
|
||||||
|
<span class="message-time">{{ message.time }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="message-text">{{ message.content }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="!message.read" class="unread-dot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
time: string;
|
time: string;
|
||||||
type: 'system' | 'notification' | 'warning';
|
type: "system" | "notification" | "warning";
|
||||||
read: boolean;
|
read: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟数据
|
// 模拟数据
|
||||||
@ -51,135 +51,139 @@ const messages = ref<Message[]>([]);
|
|||||||
|
|
||||||
// 获取消息图标
|
// 获取消息图标
|
||||||
const getMessageIcon = (type: string) => {
|
const getMessageIcon = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'system':
|
case "system":
|
||||||
return 'fa-solid fa-cog';
|
return "fa-solid fa-cog";
|
||||||
case 'notification':
|
case "notification":
|
||||||
return 'fa-solid fa-bell';
|
return "fa-solid fa-bell";
|
||||||
case 'warning':
|
case "warning":
|
||||||
return 'fa-solid fa-exclamation-triangle';
|
return "fa-solid fa-exclamation-triangle";
|
||||||
default:
|
default:
|
||||||
return 'fa-solid fa-info';
|
return "fa-solid fa-info";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标记为已读
|
// 标记为已读
|
||||||
const markAsRead = (messageId: string) => {
|
const markAsRead = (messageId: string) => {
|
||||||
const message = messages.value.find(m => m.id === messageId);
|
const message = messages.value.find((m) => m.id === messageId);
|
||||||
if (message) {
|
if (message) {
|
||||||
message.read = true;
|
message.read = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
const initMessages = () => {
|
const initMessages = () => {
|
||||||
// 这里可以调用API获取消息数据
|
// 这里可以调用API获取消息数据
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initMessages();
|
initMessages();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.message-list {
|
.message-list {
|
||||||
.message-item {
|
.message-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #f0f0f0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: #1677ff;
|
border-color: #1677ff;
|
||||||
background: #f8f9ff;
|
background: #f8f9ff;
|
||||||
}
|
|
||||||
|
|
||||||
&.unread {
|
|
||||||
border-left: 4px solid #1677ff;
|
|
||||||
background: #f0f8ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #1677ff;
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-content {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.message-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
.message-title {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #333;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-time {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-text {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.unread-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background: #1677ff;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.unread {
|
||||||
|
border-left: 4px solid #1677ff;
|
||||||
|
background: #f0f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #1677ff;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.message-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #1677ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,685 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="content-section">
|
|
||||||
<div class="content-header">
|
|
||||||
<h2 class="content-title">资源管理</h2>
|
|
||||||
<p class="content-desc">管理您的应用资源</p>
|
|
||||||
</div>
|
|
||||||
<div class="content-body">
|
|
||||||
<!-- 操作栏 -->
|
|
||||||
<div class="action-bar">
|
|
||||||
<el-button type="primary" @click="showPublishDialog = true">
|
|
||||||
<i class="fa-solid fa-plus"></i>
|
|
||||||
发布资源
|
|
||||||
</el-button>
|
|
||||||
<div class="filters">
|
|
||||||
<el-input
|
|
||||||
v-model="searchText"
|
|
||||||
placeholder="搜索资源..."
|
|
||||||
clearable
|
|
||||||
@input="handleSearch"
|
|
||||||
style="width: 300px; margin-left: 12px;"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<i class="fa-solid fa-search"></i>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 资源列表 -->
|
|
||||||
<div class="resource-list">
|
|
||||||
<div v-if="filteredResources.length === 0" class="empty-state">
|
|
||||||
<i class="fa-solid fa-cubes"></i>
|
|
||||||
<p>{{ searchText || filterType ? '未找到相关资源' : '暂无资源' }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="resource-grid">
|
|
||||||
<div
|
|
||||||
v-for="resource in paginatedResources"
|
|
||||||
:key="resource.id"
|
|
||||||
class="resource-card"
|
|
||||||
>
|
|
||||||
<div class="resource-content-area">
|
|
||||||
<div class="resource-header">
|
|
||||||
<el-tag :type="getTypeColor(resource.type)" size="small">
|
|
||||||
{{ getTypeText(resource.type) }}
|
|
||||||
</el-tag>
|
|
||||||
<h3 class="resource-title">
|
|
||||||
{{ resource.name }}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="resource-meta">
|
|
||||||
<span class="resource-category">{{ resource.category.join(', ') }}</span>
|
|
||||||
<span class="resource-downloads">{{ resource.downloads }} 下载</span>
|
|
||||||
<span class="resource-date">{{ resource.createTime }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="resource-description">
|
|
||||||
{{ resource.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="resource-actions">
|
|
||||||
<el-button size="small" @click="editResource(resource)">编辑</el-button>
|
|
||||||
<el-button size="small" type="danger" @click="deleteResource(resource)">删除</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分页组件 -->
|
|
||||||
<div v-if="filteredResources.length > 0" class="pagination-wrapper">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="currentPage"
|
|
||||||
v-model:page-size="pageSize"
|
|
||||||
:page-sizes="[5, 10, 20, 50]"
|
|
||||||
:total="totalResources"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@current-change="handlePageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 发布/编辑资源弹窗 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="showPublishDialog"
|
|
||||||
:title="isEditMode ? '编辑资源' : '发布资源'"
|
|
||||||
width="800px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
>
|
|
||||||
<el-form :model="resourceForm" label-width="80px" :rules="formRules" ref="formRef">
|
|
||||||
<el-form-item label="资源名称" prop="name" required>
|
|
||||||
<el-input
|
|
||||||
v-model="resourceForm.name"
|
|
||||||
placeholder="请输入资源名称"
|
|
||||||
maxlength="50"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="资源类型" prop="type" required>
|
|
||||||
<el-select v-model="resourceForm.type" placeholder="请选择资源类型">
|
|
||||||
<el-option
|
|
||||||
v-for="type in resourceTypes"
|
|
||||||
:key="type.value"
|
|
||||||
:label="type.label"
|
|
||||||
:value="type.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="分类" prop="category" required>
|
|
||||||
<el-checkbox-group v-model="resourceForm.category">
|
|
||||||
<el-checkbox
|
|
||||||
v-for="platform in platforms"
|
|
||||||
:key="platform.value"
|
|
||||||
:label="platform.value"
|
|
||||||
>
|
|
||||||
{{ platform.label }}
|
|
||||||
</el-checkbox>
|
|
||||||
</el-checkbox-group>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="版本号">
|
|
||||||
<el-input
|
|
||||||
v-model="resourceForm.version"
|
|
||||||
placeholder="例如:1.0.0"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="下载链接" prop="downloadUrl" required>
|
|
||||||
<el-input
|
|
||||||
v-model="resourceForm.downloadUrl"
|
|
||||||
placeholder="请输入下载链接"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="资源描述" prop="description" required>
|
|
||||||
<el-input
|
|
||||||
v-model="resourceForm.description"
|
|
||||||
type="textarea"
|
|
||||||
:rows="4"
|
|
||||||
placeholder="请输入资源描述..."
|
|
||||||
maxlength="500"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="截图上传">
|
|
||||||
<el-upload
|
|
||||||
ref="uploadRef"
|
|
||||||
:file-list="fileList"
|
|
||||||
:on-change="handleFileChange"
|
|
||||||
:on-remove="handleFileRemove"
|
|
||||||
action=""
|
|
||||||
:auto-upload="false"
|
|
||||||
multiple
|
|
||||||
accept="image/*"
|
|
||||||
list-type="picture-card"
|
|
||||||
>
|
|
||||||
<el-icon><Plus /></el-icon>
|
|
||||||
<div class="upload-text">上传截图</div>
|
|
||||||
</el-upload>
|
|
||||||
<div class="upload-tips">
|
|
||||||
<p>支持 JPG、PNG、GIF 格式,单张图片不超过 2MB,最多上传 5 张</p>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showPublishDialog = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitResource" :loading="loading">
|
|
||||||
{{ isEditMode ? '保存修改' : '发布资源' }}
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, computed, onMounted } from "vue";
|
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
|
||||||
import { Plus } from "@element-plus/icons-vue";
|
|
||||||
|
|
||||||
interface ResourceForm {
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
category: string[];
|
|
||||||
version: string;
|
|
||||||
downloadUrl: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Resource {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
category: string[];
|
|
||||||
version: string;
|
|
||||||
downloadUrl: string;
|
|
||||||
description: string;
|
|
||||||
createTime: string;
|
|
||||||
downloads: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ResourceType {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Platform {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 响应式数据
|
|
||||||
const showPublishDialog = ref(false);
|
|
||||||
const isEditMode = ref(false);
|
|
||||||
const searchText = ref('');
|
|
||||||
const filterType = ref('');
|
|
||||||
const loading = ref(false);
|
|
||||||
const formRef = ref();
|
|
||||||
const fileList = ref([]);
|
|
||||||
|
|
||||||
// 分页相关
|
|
||||||
const currentPage = ref(1);
|
|
||||||
const pageSize = ref(10);
|
|
||||||
const totalResources = computed(() => filteredResources.value.length);
|
|
||||||
|
|
||||||
// 表单数据
|
|
||||||
const resourceForm = ref<ResourceForm>({
|
|
||||||
name: '',
|
|
||||||
type: '',
|
|
||||||
category: [],
|
|
||||||
version: '',
|
|
||||||
downloadUrl: '',
|
|
||||||
description: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
// 表单验证规则
|
|
||||||
const formRules = {
|
|
||||||
name: [
|
|
||||||
{ required: true, message: '请输入资源名称', trigger: 'blur' },
|
|
||||||
{ min: 1, max: 50, message: '名称长度在 1 到 50 个字符', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
type: [
|
|
||||||
{ required: true, message: '请选择资源类型', trigger: 'change' }
|
|
||||||
],
|
|
||||||
category: [
|
|
||||||
{ type: 'array', required: true, message: '请至少选择一个分类', trigger: 'change' }
|
|
||||||
],
|
|
||||||
downloadUrl: [
|
|
||||||
{ required: true, message: '请输入下载链接', trigger: 'blur' },
|
|
||||||
{ type: 'url', message: '请输入正确的URL格式', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
description: [
|
|
||||||
{ required: true, message: '请输入资源描述', trigger: 'blur' },
|
|
||||||
{ min: 10, message: '描述不能少于10个字符', trigger: 'blur' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// 资源列表
|
|
||||||
const resources = ref<Resource[]>([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Vue DevTools',
|
|
||||||
type: 'plugin',
|
|
||||||
category: ['游戏下载'],
|
|
||||||
version: '6.5.0',
|
|
||||||
downloadUrl: 'https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd',
|
|
||||||
description: 'Vue.js 官方开发者工具,用于调试 Vue 应用。',
|
|
||||||
createTime: '2024-01-15',
|
|
||||||
downloads: 1250
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'GoLand IDE',
|
|
||||||
type: 'software',
|
|
||||||
category: ['办公下载'],
|
|
||||||
version: '2023.3',
|
|
||||||
downloadUrl: 'https://www.jetbrains.com/go/download/',
|
|
||||||
description: 'JetBrains 出品的 Go 语言集成开发环境。',
|
|
||||||
createTime: '2024-01-10',
|
|
||||||
downloads: 890
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 资源类型选项
|
|
||||||
const resourceTypes = ref<ResourceType[]>([
|
|
||||||
{ value: 'software', label: '软件工具' },
|
|
||||||
{ value: 'plugin', label: '插件扩展' },
|
|
||||||
{ value: 'library', label: '代码库' },
|
|
||||||
{ value: 'template', label: '模板素材' },
|
|
||||||
{ value: 'other', label: '其他' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 平台选项
|
|
||||||
const platforms = ref<Platform[]>([
|
|
||||||
{ value: 'windows', label: 'Windows' },
|
|
||||||
{ value: 'macos', label: 'macOS' },
|
|
||||||
{ value: 'linux', label: 'Linux' },
|
|
||||||
{ value: 'web', label: 'Web' },
|
|
||||||
{ value: 'mobile', label: '移动端' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 计算属性:过滤后的资源列表
|
|
||||||
const filteredResources = computed(() => {
|
|
||||||
let filtered = resources.value;
|
|
||||||
|
|
||||||
// 类型过滤
|
|
||||||
if (filterType.value) {
|
|
||||||
filtered = filtered.filter(resource => resource.type === filterType.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索过滤
|
|
||||||
if (searchText.value) {
|
|
||||||
filtered = filtered.filter(resource =>
|
|
||||||
resource.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
|
|
||||||
resource.description.toLowerCase().includes(searchText.value.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算属性:分页后的资源列表
|
|
||||||
const paginatedResources = computed(() => {
|
|
||||||
const start = (currentPage.value - 1) * pageSize.value;
|
|
||||||
const end = start + pageSize.value;
|
|
||||||
return filteredResources.value.slice(start, end);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 分页变化处理
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
currentPage.value = page;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSizeChange = (size: number) => {
|
|
||||||
pageSize.value = size;
|
|
||||||
currentPage.value = 1; // 重置到第一页
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取类型文本
|
|
||||||
const getTypeText = (type: string) => {
|
|
||||||
const typeObj = resourceTypes.value.find(t => t.value === type);
|
|
||||||
return typeObj ? typeObj.label : type;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取类型颜色
|
|
||||||
const getTypeColor = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'software': return 'success';
|
|
||||||
case 'plugin': return 'primary';
|
|
||||||
case 'library': return 'warning';
|
|
||||||
case 'template': return 'info';
|
|
||||||
default: return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取平台文本
|
|
||||||
const getPlatformText = (platform: string) => {
|
|
||||||
const platformObj = platforms.value.find(p => p.value === platform);
|
|
||||||
return platformObj ? platformObj.label : platform;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 搜索处理
|
|
||||||
const handleSearch = () => {
|
|
||||||
// 搜索逻辑通过计算属性实现
|
|
||||||
};
|
|
||||||
|
|
||||||
// 过滤处理
|
|
||||||
const handleFilter = () => {
|
|
||||||
// 过滤逻辑通过计算属性实现
|
|
||||||
};
|
|
||||||
|
|
||||||
// 文件变化处理
|
|
||||||
const handleFileChange = (file: any, fileList: any[]) => {
|
|
||||||
// 验证文件大小
|
|
||||||
if (file.size > 2 * 1024 * 1024) {
|
|
||||||
ElMessage.error('图片大小不能超过 2MB');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证文件数量
|
|
||||||
if (fileList.length > 5) {
|
|
||||||
ElMessage.error('最多只能上传 5 张图片');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 文件移除处理
|
|
||||||
const handleFileRemove = (file: any, fileList: any[]) => {
|
|
||||||
// 处理文件移除逻辑
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑资源
|
|
||||||
const editResource = (resource: Resource) => {
|
|
||||||
isEditMode.value = true;
|
|
||||||
resourceForm.value = {
|
|
||||||
name: resource.name,
|
|
||||||
type: resource.type,
|
|
||||||
category: [...resource.category],
|
|
||||||
version: resource.version,
|
|
||||||
downloadUrl: resource.downloadUrl,
|
|
||||||
description: resource.description
|
|
||||||
};
|
|
||||||
showPublishDialog.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 下载资源
|
|
||||||
const downloadResource = (resource: Resource) => {
|
|
||||||
window.open(resource.downloadUrl, '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除资源
|
|
||||||
const deleteResource = async (resource: Resource) => {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
`确定要删除资源"${resource.name}"吗?此操作不可恢复。`,
|
|
||||||
'确认删除',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定删除',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 这里可以调用删除API
|
|
||||||
const index = resources.value.findIndex(r => r.id === resource.id);
|
|
||||||
if (index > -1) {
|
|
||||||
resources.value.splice(index, 1);
|
|
||||||
// 检查是否需要调整当前页码
|
|
||||||
const totalPages = Math.ceil(resources.value.length / pageSize.value);
|
|
||||||
if (currentPage.value > totalPages && totalPages > 0) {
|
|
||||||
currentPage.value = totalPages;
|
|
||||||
}
|
|
||||||
ElMessage.success('资源删除成功');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 用户取消删除
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提交资源(发布或更新)
|
|
||||||
const submitResource = async () => {
|
|
||||||
if (!formRef.value) return;
|
|
||||||
|
|
||||||
await formRef.value.validate(async (valid: boolean) => {
|
|
||||||
if (!valid) return;
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isEditMode.value) {
|
|
||||||
// 更新资源
|
|
||||||
ElMessage.success('资源更新成功');
|
|
||||||
} else {
|
|
||||||
// 发布新资源
|
|
||||||
const newResource: Resource = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
name: resourceForm.value.name,
|
|
||||||
type: resourceForm.value.type,
|
|
||||||
category: [...resourceForm.value.category],
|
|
||||||
version: resourceForm.value.version || '1.0.0',
|
|
||||||
downloadUrl: resourceForm.value.downloadUrl,
|
|
||||||
description: resourceForm.value.description,
|
|
||||||
createTime: new Date().toLocaleDateString(),
|
|
||||||
downloads: 0
|
|
||||||
};
|
|
||||||
resources.value.unshift(newResource);
|
|
||||||
currentPage.value = 1; // 重置到第一页
|
|
||||||
ElMessage.success('资源发布成功');
|
|
||||||
}
|
|
||||||
|
|
||||||
showPublishDialog.value = false;
|
|
||||||
resetForm();
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('操作失败,请重试');
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
const resetForm = () => {
|
|
||||||
resourceForm.value = {
|
|
||||||
name: '',
|
|
||||||
type: '',
|
|
||||||
category: [],
|
|
||||||
version: '',
|
|
||||||
downloadUrl: '',
|
|
||||||
description: ''
|
|
||||||
};
|
|
||||||
fileList.value = [];
|
|
||||||
if (formRef.value) {
|
|
||||||
formRef.value.clearValidate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化数据
|
|
||||||
const initResources = () => {
|
|
||||||
// 这里可以调用API获取资源列表
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initResources();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.action-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-list {
|
|
||||||
.resource-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-card {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e4e7ed;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #1677ff;
|
|
||||||
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-content-area {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0; // 防止内容溢出
|
|
||||||
|
|
||||||
.resource-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
.resource-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
margin: 0;
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
|
|
||||||
.resource-version {
|
|
||||||
color: #1677ff;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-platforms {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-description {
|
|
||||||
color: #606266;
|
|
||||||
line-height: 1.6;
|
|
||||||
margin-bottom: 0;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
line-clamp: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.el-button {
|
|
||||||
width: 80px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
color: #999;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 32px;
|
|
||||||
padding: 20px 0;
|
|
||||||
|
|
||||||
.el-pagination {
|
|
||||||
::v-deep(.el-pager li) {
|
|
||||||
min-width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
line-height: 36px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep(.el-pager li.is-active) {
|
|
||||||
background-color: #1677ff;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep(.btn-prev, .btn-next) {
|
|
||||||
min-width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 弹窗表单样式 */
|
|
||||||
.el-dialog {
|
|
||||||
::v-deep(.el-dialog__body) {
|
|
||||||
padding: 20px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-form {
|
|
||||||
.el-form-item {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-tips {
|
|
||||||
margin-top: 8px;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-text {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,156 +1,168 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="content-section">
|
<div class="content-section">
|
||||||
<div class="content-header">
|
<div class="content-header">
|
||||||
<h2 class="content-title">我的钱包</h2>
|
<h2 class="content-title">我的钱包</h2>
|
||||||
<p class="content-desc">查看余额和交易记录</p>
|
<p class="content-desc">查看余额和交易记录</p>
|
||||||
</div>
|
|
||||||
<div class="content-body">
|
|
||||||
<div class="wallet-info">
|
|
||||||
<div class="balance-card">
|
|
||||||
<div class="balance-title">账户余额</div>
|
|
||||||
<div class="balance-amount">¥ {{ balance }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="transaction-history">
|
|
||||||
<h3>交易记录</h3>
|
|
||||||
<el-divider></el-divider>
|
|
||||||
<div v-if="transactions.length === 0" class="empty-state">
|
|
||||||
<i class="fa-solid fa-wallet"></i>
|
|
||||||
<p>暂无交易记录</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="transaction-list">
|
|
||||||
<div
|
|
||||||
v-for="transaction in transactions"
|
|
||||||
:key="transaction.id"
|
|
||||||
class="transaction-item"
|
|
||||||
>
|
|
||||||
<div class="transaction-info">
|
|
||||||
<div class="transaction-desc">{{ transaction.description }}</div>
|
|
||||||
<div class="transaction-time">{{ transaction.time }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="transaction-amount" :class="{ 'income': transaction.type === 'income', 'expense': transaction.type === 'expense' }">
|
|
||||||
{{ transaction.type === 'income' ? '+' : '-' }}¥{{ transaction.amount }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="content-body">
|
||||||
|
<div class="wallet-info">
|
||||||
|
<div class="balance-card">
|
||||||
|
<div class="balance-title">账户余额</div>
|
||||||
|
<div class="balance-amount">¥ {{ balance }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="transaction-history">
|
||||||
|
<h3>交易记录</h3>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
<div v-if="transactions.length === 0" class="empty-state">
|
||||||
|
<i class="fa-solid fa-wallet"></i>
|
||||||
|
<p>暂无交易记录</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="transaction-list">
|
||||||
|
<div
|
||||||
|
v-for="transaction in transactions"
|
||||||
|
:key="transaction.id"
|
||||||
|
class="transaction-item"
|
||||||
|
>
|
||||||
|
<div class="transaction-info">
|
||||||
|
<div class="transaction-desc">{{ transaction.description }}</div>
|
||||||
|
<div class="transaction-time">{{ transaction.time }}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="transaction-amount"
|
||||||
|
:class="{
|
||||||
|
income: transaction.type === 'income',
|
||||||
|
expense: transaction.type === 'expense',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ transaction.type === "income" ? "+" : "-" }}¥{{
|
||||||
|
transaction.amount
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
|
|
||||||
interface Transaction {
|
interface Transaction {
|
||||||
id: string;
|
id: string;
|
||||||
description: string;
|
description: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
time: string;
|
time: string;
|
||||||
type: 'income' | 'expense';
|
type: "income" | "expense";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模拟数据
|
// 模拟数据
|
||||||
const balance = ref('0.00');
|
const balance = ref("0.00");
|
||||||
const transactions = ref<Transaction[]>([]);
|
const transactions = ref<Transaction[]>([]);
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
const initWalletData = () => {
|
const initWalletData = () => {
|
||||||
// 这里可以调用API获取钱包数据
|
// 这里可以调用API获取钱包数据
|
||||||
balance.value = '0.00';
|
balance.value = "0.00";
|
||||||
transactions.value = [];
|
transactions.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initWalletData();
|
initWalletData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.wallet-info {
|
.wallet-info {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
|
|
||||||
.balance-card {
|
.balance-card {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.balance-title {
|
.balance-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
|
||||||
|
|
||||||
.balance-amount {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.balance-amount {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.transaction-history {
|
.transaction-history {
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.transaction-list {
|
.transaction-list {
|
||||||
.transaction-item {
|
.transaction-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
|
||||||
|
|
||||||
.transaction-info {
|
|
||||||
.transaction-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transaction-time {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.transaction-amount {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
&.income {
|
|
||||||
color: #67c23a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.expense {
|
|
||||||
color: #f56c6c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transaction-info {
|
||||||
|
.transaction-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-amount {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.income {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expense {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -10,17 +10,6 @@ import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
AutoImport({
|
|
||||||
imports: ["vue", "vue-router"],
|
|
||||||
resolvers: [ElementPlusResolver()],
|
|
||||||
dts: "src/auto-imports.d.ts",
|
|
||||||
eslintrc: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Components({
|
|
||||||
resolvers: [ElementPlusResolver()],
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@ -37,4 +26,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
port: 5000,
|
||||||
|
host: true, // 允许外部访问
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user