更新上传接口和文章发布编辑界面
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' {
|
||||
export interface GlobalComponents {
|
||||
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']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
@ -23,7 +21,6 @@ declare module 'vue' {
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
@ -36,8 +33,8 @@ declare module 'vue' {
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
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",
|
||||
"element-plus": "^2.13.0",
|
||||
"pinia": "^3.0.4",
|
||||
"quill": "^2.0.3",
|
||||
"unplugin-auto-import": "^20.3.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vue": "^3.5.24",
|
||||
|
||||
@ -1,5 +1,33 @@
|
||||
//进行接口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) {
|
||||
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 {
|
||||
/**
|
||||
* @description 获取resource文章详情
|
||||
* @description 获取resource资源详情
|
||||
* @param {string} id - 内容ID
|
||||
* @return {Promise} 返回请求结果
|
||||
*/
|
||||
static async getResourceDetail(id: string) {
|
||||
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 ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import "@/assets/less/global.less";
|
||||
import "@/assets/css/all.css"
|
||||
import App from "./App.vue";
|
||||
import router from "@/router";
|
||||
import { createPinia } from 'pinia'
|
||||
import QuillEditor from "@/views/components/QuillEditor.vue"
|
||||
|
||||
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",
|
||||
component: () => import("@/views/login/index.vue"),
|
||||
},
|
||||
{
|
||||
path: "/test-editor",
|
||||
name: "testEditor",
|
||||
component: () => import("@/views/test-editor.vue"),
|
||||
},
|
||||
{
|
||||
path: "/user/profile",
|
||||
name: "userProfile",
|
||||
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 class="profile-main">
|
||||
<div class="content-area">
|
||||
<!-- 基本资料 -->
|
||||
<BasicInfo v-if="activeMenu === 'profile-basic'" />
|
||||
|
||||
<!-- 我的钱包 -->
|
||||
<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'" />
|
||||
<!-- 子路由视图 -->
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -69,10 +51,14 @@ import BasicInfo from "@/views/user/profile/basicInfo.vue";
|
||||
import Wallet from "@/views/user/profile/wallet.vue";
|
||||
import Messages from "@/views/user/profile/messages.vue";
|
||||
import Security from "@/views/user/profile/security.vue";
|
||||
import ArticlePublish from "@/views/user/profile/articlePublish.vue";
|
||||
import ResourcePublish from "@/views/user/profile/resourcePublish.vue";
|
||||
import ListArticle from "@/views/user/profile/listArticle.vue";
|
||||
import ListResource from "@/views/user/profile/listResource.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'); // 当前激活的菜单项
|
||||
@ -149,6 +135,23 @@ const menuGroups = ref([
|
||||
// 菜单点击事件
|
||||
const handleMenuClick = (target: string) => {
|
||||
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);
|
||||
};
|
||||
|
||||
// 根据路由设置活动菜单
|
||||
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>
|
||||
<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>
|
||||
@ -14,7 +14,7 @@
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="message-item"
|
||||
:class="{ 'unread': !message.read }"
|
||||
:class="{ unread: !message.read }"
|
||||
@click="markAsRead(message.id)"
|
||||
>
|
||||
<div class="message-icon">
|
||||
@ -42,7 +42,7 @@ interface Message {
|
||||
title: string;
|
||||
content: string;
|
||||
time: string;
|
||||
type: 'system' | 'notification' | 'warning';
|
||||
type: "system" | "notification" | "warning";
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
@ -52,20 +52,20 @@ const messages = ref<Message[]>([]);
|
||||
// 获取消息图标
|
||||
const getMessageIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'system':
|
||||
return 'fa-solid fa-cog';
|
||||
case 'notification':
|
||||
return 'fa-solid fa-bell';
|
||||
case 'warning':
|
||||
return 'fa-solid fa-exclamation-triangle';
|
||||
case "system":
|
||||
return "fa-solid fa-cog";
|
||||
case "notification":
|
||||
return "fa-solid fa-bell";
|
||||
case "warning":
|
||||
return "fa-solid fa-exclamation-triangle";
|
||||
default:
|
||||
return 'fa-solid fa-info';
|
||||
return "fa-solid fa-info";
|
||||
}
|
||||
};
|
||||
|
||||
// 标记为已读
|
||||
const markAsRead = (messageId: string) => {
|
||||
const message = messages.value.find(m => m.id === messageId);
|
||||
const message = messages.value.find((m) => m.id === messageId);
|
||||
if (message) {
|
||||
message.read = true;
|
||||
}
|
||||
@ -170,6 +170,10 @@ onMounted(() => {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
i {
|
||||
font-size: 48px;
|
||||
|
||||
@ -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>
|
||||
@ -28,8 +28,16 @@
|
||||
<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
|
||||
class="transaction-amount"
|
||||
:class="{
|
||||
income: transaction.type === 'income',
|
||||
expense: transaction.type === 'expense',
|
||||
}"
|
||||
>
|
||||
{{ transaction.type === "income" ? "+" : "-" }}¥{{
|
||||
transaction.amount
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,17 +54,17 @@ interface Transaction {
|
||||
description: string;
|
||||
amount: string;
|
||||
time: string;
|
||||
type: 'income' | 'expense';
|
||||
type: "income" | "expense";
|
||||
}
|
||||
|
||||
// 模拟数据
|
||||
const balance = ref('0.00');
|
||||
const balance = ref("0.00");
|
||||
const transactions = ref<Transaction[]>([]);
|
||||
|
||||
// 初始化数据
|
||||
const initWalletData = () => {
|
||||
// 这里可以调用API获取钱包数据
|
||||
balance.value = '0.00';
|
||||
balance.value = "0.00";
|
||||
transactions.value = [];
|
||||
};
|
||||
|
||||
@ -141,6 +149,10 @@ onMounted(() => {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
i {
|
||||
font-size: 48px;
|
||||
|
||||
@ -10,17 +10,6 @@ import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: ["vue", "vue-router"],
|
||||
resolvers: [ElementPlusResolver()],
|
||||
dts: "src/auto-imports.d.ts",
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
@ -37,4 +26,8 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5000,
|
||||
host: true, // 允许外部访问
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user