添加了显示IP归属地的功能 #1

Closed
py3ni8q4o wants to merge 0 commits from master into main

5
.gitignore vendored

@ -1,5 +0,0 @@
UniLife开发进度与计划.md
UniLife接口文档.md
UniLife项目文档.md
文档说明.md
.idea/

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AugmentWebviewStateStore">
<option name="stateMap">
<map>
<entry key="CHAT_STATE" value="eyJjdXJyZW50Q29udmVyc2F0aW9uSWQiOiJjZmRkMmFjMy02YjgyLTQ5MWUtODc0YS1iOTU2MTM4NTJjODAiLCJjb252ZXJzYXRpb25zIjp7IjcxNjQ2OWJkLTU0MWUtNDZkYi1hODhlLTk2MDE1NDk4NDczYiI6eyJpZCI6IjcxNjQ2OWJkLTU0MWUtNDZkYi1hODhlLTk2MDE1NDk4NDczYiIsImNyZWF0ZWRBdElzbyI6IjIwMjUtMDUtMDVUMDQ6NTk6MTguNjg2WiIsImxhc3RJbnRlcmFjdGVkQXRJc28iOiIyMDI1LTA1LTA1VDA0OjU5OjE4LjY4NloiLCJjaGF0SGlzdG9yeSI6W10sImZlZWRiYWNrU3RhdGVzIjp7fSwidG9vbFVzZVN0YXRlcyI6e30sImRyYWZ0RXhjaGFuZ2UiOnsicmVxdWVzdF9tZXNzYWdlIjoiIiwicmljaF90ZXh0X2pzb25fcmVwciI6eyJ0eXBlIjoiZG9jIiwiY29udGVudCI6W3sidHlwZSI6InBhcmFncmFwaCJ9XX0sInN0YXR1cyI6ImRyYWZ0In0sInJlcXVlc3RJZHMiOltdLCJpc1Bpbm5lZCI6ZmFsc2UsImlzU2hhcmVhYmxlIjpmYWxzZSwiZXh0cmFEYXRhIjp7Imhhc0RpcnR5RWRpdHMiOmZhbHNlfSwicGVyc29uYVR5cGUiOjB9LCJjZmRkMmFjMy02YjgyLTQ5MWUtODc0YS1iOTU2MTM4NTJjODAiOnsiaWQiOiJjZmRkMmFjMy02YjgyLTQ5MWUtODc0YS1iOTU2MTM4NTJjODAiLCJjcmVhdGVkQXRJc28iOiIyMDI1LTA1LTA1VDA0OjU5OjE4Ljg0NloiLCJsYXN0SW50ZXJhY3RlZEF0SXNvIjoiMjAyNS0wNS0wNVQwNDo1OToxOC44NDZaIiwiY2hhdEhpc3RvcnkiOltdLCJmZWVkYmFja1N0YXRlcyI6e30sInRvb2xVc2VTdGF0ZXMiOnt9LCJkcmFmdEV4Y2hhbmdlIjp7InJlcXVlc3RfbWVzc2FnZSI6IiIsInJpY2hfdGV4dF9qc29uX3JlcHIiOnsidHlwZSI6ImRvYyIsImNvbnRlbnQiOlt7InR5cGUiOiJwYXJhZ3JhcGgifV19LCJzdGF0dXMiOiJkcmFmdCJ9LCJyZXF1ZXN0SWRzIjpbXSwiaXNQaW5uZWQiOmZhbHNlLCJpc1NoYXJlYWJsZSI6ZmFsc2UsImV4dHJhRGF0YSI6eyJoYXNEaXJ0eUVkaXRzIjpmYWxzZX0sInBlcnNvbmFUeXBlIjowfX0sImFnZW50RXhlY3V0aW9uTW9kZSI6Im1hbnVhbCIsImlzQWdlbnRFZGl0c0NvbGxhcHNlZCI6dHJ1ZSwic29ydENvbnZlcnNhdGlvbnNCeSI6Imxhc3RNZXNzYWdlVGltZXN0YW1wIn0=" />
</map>
</option>
</component>
</project>

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="unilife@localhost" uuid="9c6c9710-15d0-4710-8fca-930cc43549e9">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/unilife</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="0@127.0.0.1" uuid="a9faee19-21f5-4be8-a112-2b0ac06aaaaf">
<driver-ref>redis</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/unilife-server/src/main/resources/application.yml</remarks>
<jdbc-driver>jdbc.RedisDriver</jdbc-driver>
<jdbc-url>jdbc:redis://127.0.0.1:6379/0</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="UniLife@localhost" uuid="82d366b7-3273-49cc-8ccc-6fef4a2d408d">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/unilife-server/src/main/resources/application.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/UniLife?allowPublicKeyRetrieval=true&amp;useSSL=false&amp;serverTimezone=UTC&amp;characterEncoding=UTF-8</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/unilife-server/src/main/resources/db/init.sql" dialect="MySQL" />
</component>
</project>

@ -1,124 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

@ -1,307 +0,0 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const PACKAGE_VERSION = '2.8.6'
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
self.addEventListener('install', function () {
self.skipWaiting()
})
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
self.addEventListener('message', async function (event) {
const clientId = event.source.id
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
self.addEventListener('fetch', function (event) {
const { request } = event
// Bypass navigation requests.
if (request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
// Generate unique request ID.
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId))
})
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event)
const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
;(async function () {
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
requestId,
isMockedResponse: IS_MOCKED_RESPONSE in response,
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
body: responseClone.body,
headers: Object.fromEntries(responseClone.headers.entries()),
},
},
[responseClone.body],
)
})()
}
return response
}
// Resolve the main client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
async function getResponse(event, client, requestId) {
const { request } = event
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const requestBuffer = await request.arrayBuffer()
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: requestBuffer,
keepalive: request.keepalive,
},
},
[requestBuffer],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(
message,
[channel.port2].concat(transferrables.filter(Boolean)),
)
})
}
async function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}

@ -1,28 +0,0 @@
<template>
<HeaderBar v-if = "!route.meta.hideHeader"/>
<router-view/>
</template>
<script setup lang="ts">
import HeaderBar from './components/HeaderBar.vue'
import router from './routers/routers';
import { useRoute } from 'vue-router'
const route = useRoute()
</script>
<style>
/* 设置 body 背景渐变,清除异常布局设置 */
html, body, #app {
margin: 0;
padding: 0;
height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
background: linear-gradient(200deg, #f3e7e9, #e3eeff);
overflow: hidden;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

@ -1,195 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--light-purple:#f7f1ff;
--dark-purple: #ead1fb;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border:none;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: border-color 0.25s;
}
/*除了LogPage以外的按钮尽量使用这里的样式*/
.btn {
outline:none;
padding: 10px 24px;
margin:10px;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.3s ease;
background-color: #fff;
}
.btn-primary {
background-color: #9370DB;
color: white;
box-shadow: 0 4px 10px rgba(147, 112, 219, 0.3);
}
.btn-primary:hover {
background-color: #8a63d2;
transform: translateY(-2px);
}
.btn-secondary {
background-color: #e6e6fa;
color: #666;
box-shadow: 0 4px 10px rgba(230, 230, 250, 0.3);
}
.btn-secondary:hover {
background-color: #dcdcdc;
transform: translateY(-2px);
}
/* ======================
(.btn)
====================== */
.btn-circle {
/* 复用现有按钮基础样式 */
@apply btn btn-primary; /* 如果使用Tailwind这类工具 */
/* 新增圆形特性 */
--size: 56px;
width: var(--size);
height: var(--size);
padding: 0;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
/* 定位系统(新增) */
position: fixed;
right: 30px;
bottom: 30px;
margin: 0 !important; /* 覆盖原有margin */
/* 层级管理 */
z-index: 1000;
/* 复用现有悬停动画 */
/* 原有.btn-primary:hover已包含效果 */
}
/* 图标微调(新增) */
.btn-circle .btn-icon {
font-size: 1.8rem;
line-height: 1;
margin-top: -3px; /* 视觉居中补偿 */
}
/* 响应式调整(新增) */
@media (max-width: 768px) {
.btn-circle {
--size: 50px;
right: 15px;
bottom: 15px;
}
}
/*信息展示在card上*/
.card {
background-color: #fff;
border-radius: 20px;
padding: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
.flow-container{
padding: 4% 4% 0;
margin:0;
display: flex;
flex-direction: column;
width: 92%;
height: 100%;
overflow:auto;
}
/*输入框样式*/
.input-primary {
flex: 1;
border: 2px solid #e6e6fa;
border-radius: 25px;
outline: none;
transition: border-color 0.3s ease;
font-size: 1.2rem;
}
#app {
width: 100vw;
height: 100vh;
min-height: 100vh;
min-width: 100vw;
position: relative;
display: flex;
justify-content:center;
align-items:center;
flex-direction: column;
box-sizing: border-box;
overflow:auto;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
/* 帖子管理容器样式 */
.post-management {
padding: 30px; /* 比原来的20px更大 */
width: 100%;
max-width: 1800px; /* 比建议的1200px更宽 */
margin: 0 auto;
min-height: 80vh;
box-sizing: border-box;
/* 添加一些额外的样式让内容更突出 */
background-color: var(--light-purple);
border-radius: 20px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

@ -1,25 +0,0 @@
<script setup lang="ts">
const images = [
new URL('@/assets/logo-carousel/1.jpeg', import.meta.url).href,
new URL('@/assets/logo-carousel/2.png', import.meta.url).href,
new URL('@/assets/logo-carousel/3.jpg', import.meta.url).href
]
</script>
<template>
<el-carousel :interval="3000" height="300px" arrow="hover">
<el-carousel-item v-for="(img, i) in images" :key="i">
<img :src="img" class="carousel-img" />
</el-carousel-item>
</el-carousel>
</template>
<style scoped>
.carousel-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
</style>

@ -1,156 +0,0 @@
<script setup>
import { House, Cloudy, User, Cpu, Message, HomeFilled, MessageBox, Calendar } from '@element-plus/icons-vue'
import {useRoute,useRouter } from 'vue-router'
import { hasToken } from '@/utils/token';
import { ref } from 'vue'
const route = useRoute();
const router = useRouter();
const searchText = ref('')
const isLogin = hasToken(); //
const userInfoStr = localStorage.getItem('userInfo');
const userInfo = userInfoStr ? JSON.parse(userInfoStr) : null;
const userAvatar = userInfo?.avatar;
function goSearch() {
if (searchText.value.trim()) {
router.push({ path: '/search', query: { query: searchText.value.trim() } })
}
}
</script>
<template>
<header class ="header-bar" >
<!-- 左侧图标组 -->
<img src = "@/assets/images/logo.png" alt="Logo" class="logo" />
<div class="left-icons">
<router-link to="/unilifeHome" class="icon-btn" title="首页">
<el-icon class="icon-btn" :size="24">
<HomeFilled />
</el-icon>
</router-link>
<router-link to="/cloud" class="icon-btn" title="资料分享">
<el-icon class="icon-btn" :size="24">
<MessageBox />
</el-icon>
</router-link>
<router-link to="/personal/curriculum" class="icon-btn" title="日程">
<el-icon class = "icon-btn" :size="24">
<Calendar />
</el-icon>
</router-link>
<router-link to="/personal/ai" class="icon-btn" title="AI助手">
<el-icon class = "icon-btn" :size="24">
<Cpu />
</el-icon>
</router-link>
</div>
<!-- 新增的搜索框 -->
<div class="header-search">
<input
type="text"
v-model="searchText"
@keyup.enter="goSearch"
placeholder="搜索..."
aria-label="搜索"
/>
<button @click="goSearch"></button>
</div>
<!-- 右侧部分 -->
<div class="right-section">
<router-link to="/DirectMessage" class="icon-btn" title="消息">
<el-icon class = "icon-btn" :size = "24">
<Message/>
</el-icon>
</router-link>
<router-link to="/personal" class="user-entry" title="个人主页">
<span>个人主页</span>
</router-link>
<div v-if="!isLogin">
<router-link to="/log" class="icon-btn" title="登录">
<el-icon class = "icon-btn" :size="24">
<User />
</el-icon>
</router-link>
</div>
<div v-else>
<img :src="userAvatar" alt="User Avatar" class="user-avatar" />
</div>
</div>
</header>
</template>
<style scoped>
.header-bar {
height: 70px;
width: 100%;
background: #ead1fb;
position:fixed;
top: 0;
left:0;
padding:0;
margin:0;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
z-index: 10000;
}
.header-bar--personal {
background: linear-gradient(to top, #c9e4ff, #fad0c4);
}
.left-icons,
.right-section {
flex:7;
padding:50px;
display: flex;
align-items: center;
}
.right-section{
justify-content: flex-end;
}
.icon-btn {
margin: 0 10px;
color: #606266;
cursor: pointer;
transition: transform 0.2s;
}
.icon-btn:hover {
transform: scale(1.1);
color: #409EFF;
}
.user-entry {
margin-left: 12px;
font-weight: 600;
color: #303133;
cursor: pointer;
}
.logo
{
flex:1;
width: 120px;
height: 120px;
margin-left: 20px;
border-radius: 50%;
object-fit: cover;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
margin-left: 10px;
}
</style>

@ -1,26 +0,0 @@
<script setup lang="ts">
defineProps<{ title: string; link: string }>()
</script>
<template>
<router-link :to="link" class="hot-topic-item">
{{ title }}
</router-link>
</template>
<style lang="scss" scoped>
.hot-topic-item {
display: block;
background-color: #fbefff;
border-radius: 8px;
padding: 10px;
margin-bottom: 8px;
color: #333;
text-decoration: none;
&:hover {
background-color: #e4d4ff;
}
}
</style>

@ -1,113 +0,0 @@
<template>
<div class="markdown-body" v-html="compiledMarkdown" />
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { marked } from 'marked'
import { markedHighlight } from 'marked-highlight'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css' // 'atom-one-dark.css'
// props
const props = defineProps<{
content: string
}>()
const compiledMarkdown = ref('')
// marked
marked.use(
markedHighlight({
langPrefix: 'hljs language-',
highlight(code: string, lang: string): string {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value
}
return hljs.highlightAuto(code).value
}
})
)
// Markdown
async function renderMarkdown() {
compiledMarkdown.value = await marked.parse(props.content || '')
}
// props.content
watch(() => props.content, renderMarkdown, { immediate: true })
</script>
<style>
.markdown-body {
font-size: 16px;
line-height: 1.8;
word-break: break-word;
color: #1a1a1a;
h1, h2, h3, h4 {
font-weight: bold;
margin: 1.2em 0 0.6em;
}
h1 { font-size: 1.8em; }
h2 { font-size: 1.5em; }
h3 { font-size: 1.2em; }
a {
color: #0366d6;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
code {
background: #f6f8fa;
padding: 0.2em 0.4em;
border-radius: 4px;
font-family: 'Courier New', Courier, monospace;
}
pre {
background: #f6f8fa;
padding: 1em;
border-radius: 6px;
overflow-x: auto;
}
blockquote {
border-left: 4px solid #dfe2e5;
padding-left: 1em;
color: #6a737d;
margin: 1em 0;
}
ul, ol {
padding-left: 2em;
margin: 0.5em 0;
}
img {
max-width: 100%;
height: auto;
border-radius: 6px;
margin: 0.5em 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
th, td {
border: 1px solid #dfe2e5;
padding: 0.6em 1em;
}
th {
background-color: #f6f8fa;
}
}
</style>

@ -1,103 +0,0 @@
<template>
<div class="message-container">
<div class="left-nav">
<div class="nav-title">消息中心</div>
<ul class="nav-list">
<li
v-for="(item, index) in navItems"
:key="index"
:class="['nav-item', { active: $route.path === item.path }]"
@click="handleNavClick(item.path)"
>
<span>{{ item.text }}</span>
</li>
</ul>
</div>
<div class="center-message">
<p v-if="showWelcomeMessage">(^ω^)</p>
<router-view />
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter, useRoute } from "vue-router";
import { computed } from "vue";
const emit = defineEmits(['nav-change']);
const router = useRouter();
const route = useRoute();
const navItems = [
{ text: '私信消息', path: '/messageNav/directMessage' },
{ text: '评论回复', path: '/messageNav/comments' },
{ text: '收到的赞', path: '/messageNav/goods' },
{ text: '系统通知', path: '/messageNav/system-notification' },
];
const showWelcomeMessage = computed(() => {
return route.path === '/MessageNav';
});
const handleNavClick = (path: string) => {
router.push(path);
};
</script>
<style scoped>
.left-nav {
width: 15%;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
border: 2px solid rgba(133, 88, 207, 0.5);
border-radius: 10px;
padding: 20px 0;
margin-right: 20px;
margin-left: 100px;
height: 90%;
position: fixed;
left: 0;
top: 70px;
z-index: 9999;
}
.nav-title {
font-size: 32px;
font-weight: bold;
padding: 0 20px;
margin-bottom: 20px;
color: #333;
}
.nav-item {
padding: 15px 20px;
cursor: pointer;
transition: all 0.3s;
margin: 5px 10px;
border-radius: 8px;
font-size: 24px;
color: #333;
}
.nav-item.active {
background: rgba(156, 136, 255, 0.2);
color: #9c88ff;
font-size: 24px;
}
.nav-item:hover {
background: rgba(156, 136, 255, 0.1);
}
.center-message
{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 32px;
color:#9c88ff;
}
</style>

@ -1,237 +0,0 @@
<script set lang="ts">
import { defineComponent,ref } from 'vue';
import { useRouter,useRoute } from 'vue-router';
export default defineComponent({
name: 'Personal',
setup(){
const router = useRouter();
const route = useRoute();
// `li`
const activeIndex = ref<number>(0);
// `active`
const setActive = (index: number) => {
activeIndex.value = index; // Vue `active`
};
return {
activeIndex,
router,
route,
setActive,
};
}
});
</script>
<template>
<router-view/>
<div class = "shell">
<ul class="nav">
<li :class="{active: route.name === 'Home'}" @click ="setActive(0)" id = "avatar">
<router-link :to="{name:'Home'}">
<div class="icon">
<div class="imageBox">
<img src="@/assets/images/默认头像.jpg">
</div>
</div>
<div class="text">测试样例</div>
</router-link>
</li>
<li :class="{active:route.name === 'Manager'}" @click="setActive(1)">
<router-link :to="{name:'Manager'}">
<div class="icon">
<div class="imageBox">
<img src="@/assets/images/个人.png">
</div>
</div>
<div class="text">账号管理</div>
</router-link>
</li>
<li :class="{active:route.name === 'Curriculum'}" @click="setActive(2)">
<router-link :to="{name:'Curriculum'}">
<div class="icon">
<div class="imageBox">
<img src="@/assets/images/个人.png">
</div>
</div>
<div class="text">个人课表</div>
</router-link>
</li>
<li :class="{active:route.name === 'PostManager'}" @click="setActive(3)">
<router-link :to="{name:'PostManager'}">
<div class="icon">
<div class="imageBox">
<img src="@/assets/images/个人.png">
</div>
</div>
<div class="text">帖子管理</div>
</router-link>
</li>
<li :class="{active:route.name === 'AIManager'}" @click="setActive(4)">
<router-link :to="{name:'AIManager'}">
<div class="icon">
<div class="imageBox">
<img src="@/assets/images/个人.png">
</div>
</div>
<div class="text">AI助手</div>
</router-link>
</li>
</ul>
</div>
</template>
<style scoped>
*{
margin:0;
padding:0;
box-sizing : border-box;
list-style:none;
text-decoration:none;
}
section{
position:relative;
width:100%;
height:100vh;
display:flex;
justify-content:center;
align-items:center;
font:900 100px '';
color:rgba(175, 90, 240, 0.308);
background-color: #e4e9f5;
}
.shell{
position:fixed;
top:0%;
left:0%;
width:84px;
height:100%;
background-color:#ead1fb;
z-index:9999;
transition:width 0.5s;
padding-left:10px;
overflow:hidden;
}
.shell:hover{
width:300px;
}
.imageBox{
position:relative;
width:50px;
height:50px;
border-radius:50%;
overflow:hidden;
}
.imageBox img{
width:100%;
height:100%;
object-fit:cover;
}
.shell ul{
position:relative;
height:100vh;
}
.shell ul li{
position:relative;
padding:7px;
}
.active{
background-color: #fff;
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
}
.active::before{
content:"";
position:absolute;
top:-30px;
right:0;
width:30px;
height:30px;
border-bottom-right-radius:25px;
box-shadow:5px 5px 0 5px #fff;
background:transparent;
}
.active::after{
content:"";
position:absolute;
bottom:-30px;
right:0;
width:30px;
height:30px;
border-top-right-radius: 25px;
box-shadow:5px -5px 0 5px #fff;
background:transparent;
}
#avatar{
margin:100px 0 100px 0;
}
.shell ul li a{
position:relative;
display:flex;
white-space:nowrap;
}
.icon{
position:relative;
display:flex;
justify-content:center;
align-items: center;
min-width:60px;
padding-left:10px;
height:70px;
color:#333;
transition:0.5s;
color: rgb(153, 109, 240)
}
.icon i{
font-size:30px;
z-index:999;
}
.text{
position:relative;
height:70px;
display:flex;
align-items:center;
font-size:20px;
color:#333; /*字体颜色 */
padding-left:15px;
text-transform:uppercase;
letter-spacing:2px;/*字体间距*/
transition:0.5s;
}
.shell ul li:hover a .icon,
.shell ul li:hover a .text
{
color: #f3e7e9;/*字体和图标被选中后的颜色 */
}
.active a .icon::before{
content:"";
position:absolute;
inset:5px;
width:60px;
background:#fff;
border-radius:50%;
transition:0.5s;
border:7px solid rgb(110,90,240);
box-sizing:border-box;
}
</style>

@ -1,41 +0,0 @@
<script setup lang="ts">
defineProps<{
post: {
title: string
tags: string[]
excerpt: string
link: string
}
}>()
</script>
<template>
<router-link :to="post.link" class="post-card">
<h3>{{ post.title }}</h3>
<div class="tags">
<el-tag v-for="(tag, i) in post.tags" :key="i" type="info">{{ tag }}</el-tag>
</div>
<p>{{ post.excerpt }}</p>
</router-link>
</template>
<style lang="scss" scoped>
.post-card {
display: block;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
transition: transform 0.2s;
color: inherit;
text-decoration: none;
&:hover {
transform: translateY(-2px);
}
.tags {
margin: 8px 0;
}
}
</style>

@ -1,63 +0,0 @@
<template>
<div class="author-box">
<img class="avatar" :src="author.avatar" @click="toProfile" />
<div class="nickname">{{ author.name }}</div>
<div class="stats">
<span>xx 关注</span>
<span>xx 粉丝</span>
<span>xx 帖子</span>
</div>
<div class="other-posts">
<h4>这是帖主的其他帖子</h4>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
author: {
id: number,
name: string,
avatar: string,
bio: string,
},
}>()
function toProfile() {
window.location.href = '/not-found'
}
</script>
<style scoped lang="scss">
.author-box {
text-align: center;
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
margin-bottom: 8px;
cursor: pointer;
}
.nickname {
font-weight: bold;
margin-bottom: 8px;
}
.stats {
display: flex;
justify-content: space-around;
margin-bottom: 16px;
font-size: 14px;
}
.other-posts {
background: #fff;
border-radius: 8px;
padding: 10px;
font-size: 14px;
text-align: left;
}
}
</style>

@ -1,90 +0,0 @@
<template>
<div class="post-content">
<!-- 顶部信息 -->
<div class="post-header">
<img class="avatar" :src="post.author.avatar" @click="toProfile" />
<div class="info">
<div class="nickname">{{ post.author.name }}</div>
<div class="meta">发布日期 | IP 属地</div>
</div>
<button class="follow-btn btn-primary" @click="toProfile">+</button>
</div>
<el-divider />
<!-- markdown内容 -->
<MarkdownContent :content="post.content" />
<!-- 评论区 -->
<CommentInput />
</div>
</template>
<script setup lang="ts">
import { string } from 'yup';
import MarkdownContent from './PostContent/MarkdownContent.vue'
import CommentInput from './PostContent/CommentInput.vue'
import CommentList from './PostContent/CommentList.vue'
defineProps<{
post: {
id:number,
title:string,
content:string,
author: {
id: number,
name: string,
avatar: string,
bio: string,
},
tags:string[],
},
}>()
function toProfile() {
window.location.href = '/not-found'
}
</script>
<style scoped lang="scss">
.post-content{
.post-header {
display:flex;
min-height:70px;
align-items: center;
margin-bottom: 30px;
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
cursor: pointer;
}
.info {
margin-left: 15px;
.nickname {
font-size: 25px;
font-weight: bold;
}
.meta {
font-size: 15px;
}
}
.follow-btn {
width:100px;
height:50px;
margin-left: auto;
margin-right:50px;
}
}
}
</style>

@ -1,133 +0,0 @@
<template>
<div class="comment-section">
<!-- 评论输入框 -->
<div class="comment-input">
<img class="avatar" src="@/assets/images/默认头像.jpg" @click="goToProfile" />
<input v-model="newComment" placeholder="发布友善的评论" />
<button @click="submitComment">
<i class="icon-send">📨</i>
</button>
</div>
<!-- 评论操作栏 -->
<div class="action-bar">
<i class="icon">👍</i>
<i class="icon">📤</i>
<i class="icon"></i>
</div>
<!-- 评论列表 -->
<div class="comment-list">
<CommentList
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CommentList from './CommentList.vue'
import Avatar from '@/assets/images/默认头像.jpg';
interface Comment {
id: number
user: string
avatar: string
content: string
date: string
}
const newComment = ref('')
const comments = ref<Comment[]>([
{
id: 1,
user: '其他用户昵称',
avatar: Avatar,
content: '这是一条评论……',
date: '日期时间 IP',
},
{
id: 2,
user: '用户B',
avatar: Avatar,
content: '第二条评论',
date: '日期时间 IP',
},
])
function goToProfile() {
window.location.href = '/not-found'
}
function submitComment() {
if (!newComment.value.trim()) return
comments.value.push({
id: Date.now(),
user: '你',
avatar: Avatar,
content: newComment.value,
date: '刚刚',
})
newComment.value = ''
}
</script>
<style scoped lang="scss">
.comment-section {
margin-top: 24px;
.comment-input {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
}
input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 6px;
}
button {
color: white;
border: none;
border-radius: 6px;
padding: 6px 10px;
cursor: pointer;
}
}
.action-bar {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-bottom: 16px;
.icon {
cursor: pointer;
font-size: 20px;
&:hover {
}
}
}
.comment-list {
display: flex;
flex-direction: column;
gap: 12px;
}
}
</style>

@ -1,84 +0,0 @@
<template>
<div class="comment-item">
<img class="avatar" :src="comment.avatar" @click="toProfile" />
<div class="content-box">
<div class="user">{{ comment.user }}</div>
<div class="content">{{ comment.content }}</div>
<div class="meta">
<span class="date">{{ comment.date }}</span>
<span class="actions">
<i class="icon" title="点赞">👍</i>
<i class="icon" title="回复">💬</i>
<i class="icon" title="举报">🚩</i>
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
comment: {
id: number
user: string
avatar: string
content: string
date: string
}
}>()
function toProfile() {
window.location.href = '/not-found'
}
</script>
<style scoped lang="scss">
.comment-item {
display: flex;
gap: 10px;
padding: 12px;
border: 1px solid #eee;
border-radius: 6px;
background: #fff;
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
cursor: pointer;
}
.content-box {
flex: 1;
.user {
font-weight: bold;
margin-bottom: 4px;
}
.content {
margin-bottom: 6px;
font-size: 14px;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 12px;
.actions {
display: flex;
gap: 10px;
.icon {
cursor: pointer;
&:hover {
}
}
}
}
}
}
</style>

@ -1,34 +0,0 @@
<template>
<div class="markdown-body">
<MarkdownRender :content="content" />
</div>
</template>
<script setup lang="ts">
import MarkdownRender from '@/components/MarkdownRender.vue';
defineProps<{
content: string
}>();
</script>
<style scoped lang="scss">
.markdown-body {
font-family: 'Georgia', serif;
line-height: 1.6;
h1 {
font-size: 24px;
margin-bottom: 12px;
}
p {
margin: 10px 0;
}
img {
max-width: 100%;
margin: 10px 0;
border-radius: 6px;
}
}
</style>

@ -1,60 +0,0 @@
<template>
<div class="post-sidebar">
<h2>分类</h2>
<ul class="top-tags">
<li v-for="tag in tags" :key="tag" @click="goTo(tag)">
{{ tag }}
</li>
</ul>
<el-divider />
<div class="sub-menu">
<div v-for="item in subs" :key="item" @click="goTo(item)">{{ item }}</div>
</div>
</div>
</template>
<script setup lang="ts">
const tags = ['计算机学院', '遥感学院', '电子信息学院']
const subs = ['综合', '最新', '热度最高', '用户']
//
function goTo(tag: string) {
// NotFound
window.location.href = '/not-found'
}
</script>
<style scoped lang="scss">
.post-sidebar {
font-size: 20px;
.top-tags {
margin-top: 8px;
padding: 0;
li {
cursor: pointer;
margin: 6px 0;
list-style-type: none;
padding: 0;
&:hover {
font-size:24px;
color:#8a63d2;
}
}
}
.sub-menu {
margin-top: 24px;
div {
margin: 6px 0;
cursor: pointer;
&:hover {
font-size:24px;
color:#8a63d2;
}
}
}
}
</style>

@ -1,19 +0,0 @@
<script setup lang="ts">
</script>
<template>
<div class="schedule-placeholder">
<p>此处预留今日行程组件位置</p>
</div>
</template>
<style scoped>
.schedule-placeholder {
background: #f5f0ff;
border-radius: 8px;
padding: 16px;
min-height: 200px;
text-align: center;
color: #999;
}
</style>

@ -1,27 +0,0 @@
import request from "../../src/utils/request"
export function useEmailCode(){
const sendEmailCode = async(email:string) =>
{
return await request.post('/user/code',
{
params:{email:email}
}
)
}
const verifyEmailCode = async(email:string,code:string) =>
{
return await request.post('users/login/code',
{
params:{email:email,code:code}
}
)
}
return{
sendEmailCode,
verifyEmailCode
}
}

@ -1,34 +0,0 @@
import { createApp } from 'vue'
import '@/assets/style/style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './routers/routers'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
async function prepareApp() {
if (import.meta.env.DEV) {
const { worker } = await import('./mocks/browser');
await worker.start(); // 确保 MSW 启动完成
}
const app = createApp(App);
app.use(router);
app.use(ElementPlus);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.mount('#app');
}
prepareApp();
// const app = createApp(App)
// app.use(ElementPlus)
// app.use(router)
// for(const [key, component] of Object.entries(ElementPlusIconsVue)) {
// app.component(key, component)
// }
// app.mount('#app')

@ -1,4 +0,0 @@
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers.ts';
export const worker = setupWorker(...handlers);

@ -1,116 +0,0 @@
import { http,HttpResponse } from 'msw';
import Avatar from '@/assets/images/默认头像.jpg'
import { Briefcase } from '@element-plus/icons-vue';
let mockUserData = {
userId: 1,
username: '测试员',
email: "test@example.com",
nickname: '测试员',
avatar: Avatar,
bio:"只要不出bug一切都好QAQ",
gender:'1',
birthday: '2000-01-01',
studentId:20220101001,
department: "计算机学院",
major: "软件工程",
grade: "2023级",
points: 100,
role: 0,
status:1,
isVerified: true,
};
export const handlers = [
http.post('/users/login', async ({request}) => {
const body = await request.json() as { data: { username: string; password: string } };
if (body.data.username === 'test@example.com' && body.data.password === '123456') {
return HttpResponse.json({
code: 200,
message: '登录成功',
data: {
token: 'mock-jwt-token-123',
userInfo: {
userId: mockUserData.userId,
username: mockUserData.username,
nickname: mockUserData.nickname,
avatar: mockUserData.avatar,
role: mockUserData.role,
isVerified: mockUserData.isVerified,
status: mockUserData.status,
}
}
});
}
return HttpResponse.json({ message: '用户名或密码错误' }, { status: 401 });
}),
//3.1获取用户个人信息
http.get('/users/info', async ({ request }) => {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (token === 'mock-jwt-token-123') {
return HttpResponse.json({
code: 200,
message: '获取用户信息成功',
data: mockUserData,
});
}
return HttpResponse.json({ message: '未授权' }, { status: 401 });
}),
//3.2更新用户个人信息
http.put('/users/profile', async ({ request }) => {
const body = await request.json() as typeof mockUserData;
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (token === 'mock-jwt-token-123') {
// 更新 mock 数据
mockUserData = {
...mockUserData,
...body,
};
return HttpResponse.json({
code: 200,
message: '更新成功',
data: null,
});
}
return HttpResponse.json({ code: 401, message: '未授权' }, { status: 401 });
}),
//3.3修改用户密码
http.put('/users/password', async ({ request }) => {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (token === 'mock-jwt-token-123') {
return HttpResponse.json({
code: 200,
message: '密码修改成功',
data: null,
});
}
return HttpResponse.json({ code: 401, message: '未授权' }, { status: 401 });
}),
//3.4上传用户头像
http.post('/users/avatar', async ({ request }) => {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
const body = await request.json() as { avatar: string };
if (token === 'mock-jwt-token-123') {
mockUserData.avatar = body.avatar;
// 模拟头像上传成功;
return HttpResponse.json({
code: 200,
message: '头像上传成功',
data: {
avatar: mockUserData.avatar,
},
});
}
return HttpResponse.json({ code: 401, message: '未授权' }, { status: 401 });
}),
];

@ -1,122 +0,0 @@
import type { RouteRecord, RouteRecordRaw } from 'vue-router';
import { createWebHashHistory, createRouter,createWebHistory } from 'vue-router';
import LogPage from '../views/LogPage.vue';
import Personal from '@/components/Personal.vue';
import Manager from '@/views/AcountManager.vue';
import PersonalHome from '@/views/Home.vue';
import ForumHome from '@/views/ForumHome.vue';
import PostManager from '@/views/PostManagement.vue';
import Curriculum from '@/views/Curriculum.vue';
import DirectMessage from '@/views/DirectMessage.vue';
import AIManager from '@/views/AiManager.vue'
import MessageNav from '@/components/MessageNav.vue'
import Comments from '@/views/Comments.vue'
import Goods from '@/views/Goods.vue'
import SystemNotifications from '@/views/System-notifications.vue'
import SearchResult from "@/views/SearchResult.vue";
const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/log',
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/404.vue'),
},
{
path: '/log',
name: 'LogPage',
component: LogPage
},
{
path: '/personal',
name: 'Personal',
component: Personal,
children: [
{
path: '',
name: 'Home',
component: PersonalHome,
},
{
path: 'manager',
name: 'Manager',
component: Manager,
},
{
path:'ai',
name:'AIManager',
component:AIManager,
},
{
path: 'curriculum',
name: 'Curriculum',
component: Curriculum,
},
{
path:'postManager',
name:'PostManager',
component:PostManager,
}
]
},
{
path:'/uniLifeHome',
name: 'ForumHome',
component: ForumHome,
},
{
path: '/post/:id',
name: 'PostDetail',
component: () => import('@/views/PostDetailPage.vue'),
},
{
path:'/postEdit',
name:'PostEdit',
component:() => import('@/views/PostEditView.vue'),
meta:{
hideHeader:true
}
},
{
path:'/messageNav',
name:'MessageNav',
component:MessageNav,
children: [
{
path: 'directMessage',
name: 'DirectMessage',
component: DirectMessage,
},
{
path: 'comments',
name: 'Comments',
component: Comments,
},
{
path: 'goods',
name: 'Goods',
component: Goods,
},
{
path: 'system-notification',
name: 'System-notification',
component: SystemNotifications,
}
]
},
{
path: '/search',
name: 'SearchResult',
component: SearchResult,
}
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
});
export default router;

@ -1,149 +0,0 @@
// api/post.ts
import request from './request'
import type { Post, PostListParams, PostListResponse } from '@/views/post'
export const postApi = {
// 获取我的帖子列表
getMyPosts(params: PostListParams): Promise<PostListResponse> {
return request({
url: '/posts/my-posts',
method: 'GET',
params
})
},
// 获取帖子详情
getPostDetail(id: number): Promise<Post> {
return request({
url: `/posts/${id}`,
method: 'GET'
})
},
// 创建帖子
createPost(data: Partial<Post>): Promise<Post> {
return request({
url: '/posts',
method: 'POST',
data
})
},
// 更新帖子
updatePost(id: number, data: Partial<Post>): Promise<Post> {
return request({
url: `/posts/${id}`,
method: 'PUT',
data
})
},
// 删除单个帖子
deletePost(id: number): Promise<void> {
return request({
url: `/posts/${id}`,
method: 'DELETE'
})
},
// 批量删除帖子
batchDeletePosts(ids: number[]): Promise<void> {
return request({
url: '/posts/batch-delete',
method: 'DELETE',
data: { ids }
})
},
// 发布帖子(从草稿状态发布)
publishPost(id: number): Promise<Post> {
return request({
url: `/posts/${id}/publish`,
method: 'POST'
})
},
// 将帖子设为草稿
draftPost(id: number): Promise<Post> {
return request({
url: `/posts/${id}/draft`,
method: 'POST'
})
},
// 获取帖子统计信息
getPostStats(): Promise<{
totalPosts: number
totalViews: number
totalLikes: number
totalComments: number
todayPosts: number
weekPosts: number
monthPosts: number
}> {
return request({
url: '/posts/stats',
method: 'GET'
})
},
// 点赞帖子
likePost(id: number): Promise<{ liked: boolean; likesCount: number }> {
return request({
url: `/posts/${id}/like`,
method: 'POST'
})
},
// 取消点赞
unlikePost(id: number): Promise<{ liked: boolean; likesCount: number }> {
return request({
url: `/posts/${id}/unlike`,
method: 'POST'
})
},
// 增加浏览量
increaseViews(id: number): Promise<{ views: number }> {
return request({
url: `/posts/${id}/view`,
method: 'POST'
})
},
// 搜索帖子
searchPosts(params: {
keyword: string
category?: string
page?: number
pageSize?: number
}): Promise<PostListResponse> {
return request({
url: '/posts/search',
method: 'GET',
params
})
},
// 获取热门帖子
getHotPosts(params: {
period?: 'day' | 'week' | 'month'
limit?: number
} = {}): Promise<Post[]> {
return request({
url: '/posts/hot',
method: 'GET',
params
})
},
// 获取推荐帖子
getRecommendedPosts(limit = 10): Promise<Post[]> {
return request({
url: '/posts/recommended',
method: 'GET',
params: { limit }
})
}
}
//

@ -1,38 +0,0 @@
import axios from 'axios';
import {getToken} from '@/utils/token';
const service = axios.create({
baseURL: '', //改成msw测试如果要测试真实的后端记得改回去
timeout: 5000
});
service.interceptors.request.use(
config => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
console.log("前端发送信息");
return config;
} else {
console.log("没有token");
return config;
}
},
error => {
// 对请求错误做些什么
return Promise.reject(error);
}
);
service.interceptors.response.use(
response => {
console.log("后端返回信息");
return response.data;
},
error => {
// 对响应错误做些什么
return Promise.reject(error);
}
);
export default service;

@ -1,41 +0,0 @@
export function formatTime(timestamp: number): string {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - timestamp;
// 不到1分钟
if (diff < 60 * 1000) {
return '刚刚';
}
// 不到1小时
if (diff < 60 * 60 * 1000) {
const minutes = Math.floor(diff / (60 * 1000));
return `${minutes}分钟前`;
}
// 不到24小时
if (diff < 24 * 60 * 60 * 1000) {
const hours = Math.floor(diff / (60 * 60 * 1000));
return `${hours}小时前`;
}
// 不到7天
if (diff < 7 * 24 * 60 * 60 * 1000) {
const days = Math.floor(diff / (24 * 60 * 60 * 1000));
return `${days}天前`;
}
// 同一年
if (date.getFullYear() === now.getFullYear()) {
return `${date.getMonth() + 1}${date.getDate()}${padZero(date.getHours())}:${padZero(date.getMinutes())}`;
}
// 其他情况显示完整日期
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}${padZero(date.getHours())}:${padZero(date.getMinutes())}`;
}
// 补零函数
function padZero(num: number): string {
return num < 10 ? `0${num}` : num.toString();
}

@ -1,17 +0,0 @@
const TOKEN_KEY = 'forum_token'; // 存储 key可统一管理
export const setToken = (token: string) => {
localStorage.setItem(TOKEN_KEY, token);
};
export const getToken = (): string | null => {
return localStorage.getItem(TOKEN_KEY);
};
export const removeToken = () => {
localStorage.removeItem(TOKEN_KEY);
};
export const hasToken = (): boolean => {
return !!getToken();
};

@ -1,44 +0,0 @@
<template>
<div class="not-found-container">
<el-icon class="not-found-icon"><Warning /></el-icon>
<h1>404 - 页面未找到</h1>
<p>你访问的页面不存在可能是链接失效或地址错误</p>
<button class = "btn btn-primary" @click="goHome"></button>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { Warning } from '@element-plus/icons-vue'
const router = useRouter()
const goHome = () => {
router.push('/') // '/log'
}
</script>
<style scoped>
.not-found-container {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
.not-found-icon {
font-size: 64px;
color: #b19cd9;
margin-bottom: 20px;
}
h1 {
margin-bottom: 10px;
}
p {
margin-bottom: 30px;
}
</style>

@ -1,919 +0,0 @@
<script set lang="ts">
import { defineComponent, ref, nextTick ,watch,onMounted} from 'vue';
import{useForm,useField,Form} from 'vee-validate';
import * as yup from 'yup';
import request from '@/utils/request';
import { useGetDerivedNamespace ,ElMessage} from 'element-plus';
import { useEmailCode } from '@/components/useEmailCode';
export default defineComponent({
name: 'Manager',
//
// usernamegenderintroductionbirthdayemail,password()
//email
//usernamegenderintroductionbirthday
//
//使struct orignData{username,gender,introduction,birthday,email,password}
//使ToolTip
setup() {
const originData = ref({
userId: '',
username: '',
email: '',
nickname: '',
avatar: '',
bio: '',
gender: '',
birthday: '',
studentId: '',
department: '',
major: '',
grade: '',
points: '',
role: '',
isVerified: true,
});
const formData = ref({...originData.value});//
//
const isEditable = ref(false);
//
const showErrors = ref(false);
const PasswordEdit = ref(false);
const newpassword = ref('');
const newpasswordConfirm = ref('');
const emailCode = ref(''); //
const toggleEdit = () => {
isEditable.value = !isEditable.value;
};
const togglePasswordEdit = () => {
PasswordEdit.value = !PasswordEdit.value;
};
//
const hover = ref(false)
const dialogVisible = ref(false);
const previewUrl = ref<string>('');
const selectedFile = ref<File | null>(null)
const handleAvatarClick = ()=>
{
dialogVisible.value = true;
}
const beforeAvatarUpload = (file:File)=>
{
const isImage = file.type.startsWith('image/');
if(!isImage){
ElMessage.error('只能上传图片文件')
}
const isLt2M = file.size / 1024 / 1024 <2
if(!isLt2M)
{
ElMessage.error('图片大小不能超过2MB')
}
if(isImage && isLt2M)
{
selectedFile.value = file;
previewUrl.value = URL.createObjectURL(file)
}
return false
}
const uploadAvatar = async() =>
{
if(!selectedFile.value) return
const formData = new FormData();
formData.append('file',selectedFile.value);
try{
const res = await request.post('/users/avatar',
{
data:{
file:formData,
}
})
if(res.data.code === 200)
{
ElMessage.success('头像上传成功')
previewUrl.value = URL.createObjectURL(selectedFile.value)
}
else
{
ElMessage.error('头像上传失败')
}
}catch(error){
console.error(error)
ElMessage.error('头像上传失败')
}
}
const submitAvatar = () =>
{
uploadAvatar();
}
//alertbug
const errorsMeg = ref('');
//
//
const ProfileScheme = useForm({
initialValues:formData.value,
validationSchema:yup.object(
{
username:yup.string().required('昵称不能为空'),
bio:yup.string().required("自我介绍不能为空"),
}
)
})
//
const PasswordSheme = useForm({
initialValues:{
newpassword:newpassword.value,
newpasswordConfirm:newpasswordConfirm.value,
emailCode:emailCode.value
},
validationSchema:yup.object(
{
newpassword:yup.string().required('新密码不能为空').min(6,'密码至少6位'),
newpasswordConfirm:yup.string().required('确认密码不能为空').oneOf([yup.ref('newpassword')],'两次输入的密码不一致'),
emailCode:yup.string().required('验证码不能为空')
}
)
})
//
//
const onProfileSubmit = async() =>{
console.log("调用个人信息提交函数");
ProfileScheme.resetForm({values:{...formData.value},
errors:{}});
PasswordSheme.resetForm({errors:{}});
//
ProfileScheme.handleSubmit((values)=>{
console.log("表单调用成功",values);
updataUserInfo().then((res)=>
{
if(res.code == 200){
console.log("个人信息保存成功");
isEditable.value = !isEditable;
}
else
{
console.log("个人信息保存失败")
}
})
},(errors) => {
console.log("表单调用失败", errors);
formData.value = { ...originData.value }; //
}
)();
}
//
const onPasswordSubmit = async()=>
{
console.log("调用密码提交函数");
PasswordSheme.resetForm({values:{
newpassword:newpassword.value,
newpasswordConfirm:newpasswordConfirm.value,
emailCode:emailCode.value
},errors:{}});
ProfileScheme.resetForm({errors:{}});
PasswordSheme.handleSubmit((values)=>{
console.log("表单调用成功",values);
modifyPassword().then((res)=>{
if(res != 200){
ElMessage.error("验证码错误");
}
else{
console.log("修改成功");
PasswordEdit.value = false;
}
})
},(err) =>{
console.log("表单调用失败",err);
})();
}
//
//axios
//
async function getUserInfo() {
try {
const response = await request.get('/users/info');
console.log('获取用户信息成功:', response);
//
originData.value = response.data;
//formData
formData.value = { ...originData.value };
} catch (error) {
console.error('获取用户信息失败:', error);
}
}
//
async function updataUserInfo(){
try{
const res = request.put('/users/profile',{
data:{
username:formData.value.username,
bio:formData.value.bio,
gender:formData.value.gender,
birthday:formData.value.birthday,
}
})
console.log("向后端发送数据成功:",formData.value);
return (await res);
}
catch(error)
{
console.log("发送数据失败",error);
}
}
//
const {sendEmailCode} = useEmailCode()
async function modifyPassword()
{
try{
const res = await request.put('/users/password',{
data:{
code:emailCode,
newPassword:newpassword
}
})
if(res.data.code == 200) console.log("新密码修改成功")
else console.log("验证码错误");
return res.data
} catch (error) {
console.error('Password modification failed:', error);
}
}
onMounted(()=>{
getUserInfo();
})
return {
originData,
formData,
isEditable,
newpassword,
newpasswordConfirm,
emailCode,
sendEmailCode,
PasswordEdit,
toggleEdit,
togglePasswordEdit,
//
ProfileScheme,
PasswordSheme,
onProfileSubmit,
onPasswordSubmit,
//
showErrors,
errorsMeg,
//
hover,
dialogVisible,
previewUrl,
selectedFile,
handleAvatarClick,
beforeAvatarUpload,
uploadAvatar,
submitAvatar
};
}
});
</script>
<template>
<!--错误信息显示的地方-->
<transition name = "fade-up">
<el-alert
class = "error-msg"
v-if="showErrors"
:title= "errorsMeg"
type="error"
effect="dark"
:closable="true"
@close="showErrors = false"
show-icon = "true"
center/>
</transition>
<div class = "container">
<div class="main-content">
<!-- 左侧信息设置区 -->
<div class="card profile-info-card">
<!-- 个人信息部分 -->
<Form :validation-schema="ProfileScheme">
<div class="form-section">
<h2 class="section-title">个人信息</h2>
<div class="form-group">
<label class="form-label">昵称</label>
<input type="text" class="form-input" v-model="formData.username" :readonly="!isEditable"/>
</div>
<div class="form-group">
<label class="form-label">个人简介</label>
<input type="text" class="form-input" v-model="formData.bio" :readonly="!isEditable">
</div>
<div class="form-group">
<label class="form-label">性别</label>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="gender" class="radio-input" v-model = "formData.gender" :value="2" :disabled="!isEditable">
<span></span>
</label>
<label class="radio-label">
<input type="radio" name="gender" class="radio-input" v-model = "formData.gender" :value="1" :disabled="!isEditable">
<span></span>
</label>
</div>
</div>
<div class="form-group">
<label class="form-label">生日</label>
<input type="date" class="date-input" v-model = "formData.birthday" :readonly="!isEditable">
</div>
</div>
<div class="btn-save-container">
<button type = "submit" class="btn btn-primary" @click.prevent="onProfileSubmit()" :disabled = "!isEditable"> </button>
<button type = "button"class="btn btn-secondary" @click="toggleEdit">{{ isEditable ? '' : ' ' }}</button>
</div>
</Form>
<div class="divider"></div>
<!-- 账号信息部分 -->
<Form :validation-schema="PasswordSheme">
<div class="form-section">
<h2 class="section-title">账号信息</h2>
<!--展示邮箱同时提供发送验证码的按钮-->
<div class="form-group">
<label class="form-label">绑定邮箱</label>
<input type="email" class="form-input" v-model="formData.email" readonly>
<button class="btn btn-primary" v-if="PasswordEdit" @click = "sendEmailCode(formData.email)"></button>
</div>
<div class="form-group">
<label class="form-label">修改密码</label>
<div class="password-inputs" v-if = "!PasswordEdit">
<input type="password" class="form-input" v-model = "formData.email" readonly>
</div>
<div class = "password-inputs" v-if = "PasswordEdit">
<input type="password" class="form-input" v-model = "newpassword" placeholder="新密码"/>
<input type="password" class="form-input" v-model = "newpasswordConfirm" placeholder="确认新密码"/>
<input type="email-code" class="form-input" v-model="emailCode" placeholder="验证码">
</div>
<button type ="button" class="btn btn-primary btn-password" @click = "togglePasswordEdit()">
{{ PasswordEdit ? '取消' : '修改' }}
</button>
<button type = "submit" class="btn btn-secondary btn-password" v-if="PasswordEdit" @click.prevent="onPasswordSubmit()">
保存
</button>
</div>
<p class="form-hint">* 密码至少包含6个字符建议使用字母数字和符号的组合</p>
</div>
</Form>
</div>
<!-- 右侧预览区 -->
<div class="card profile-preview-card">
<div class="preview-section">
<div class="avatar-wrapper" @click = "handleAvatarClick" @mouseenter = "hover = true" @mouseleave = "hover = false">
<img :src="originData.avatar" alt="用户头像" class = "avatar-image">
<div v-if = "hover" class = "avatar-hover-mask">更换头像</div>
</div>
<el-dialog
v-model = "dialogVisible"
title = "更换头像"
width="70%"
:show-close="false">
<el-upload
class="avatar-uploader"
:http-request = "uploadAvatar"
:show-file-list="false"
:before-upload = "beforeAvatarUpload"
>
<img v-if="previewUrl" :src="previewUrl" class="avatar-preview"/>
<el-icon size = 30px v-else><Plus/></el-icon>
</el-upload>
<template #footer>
<el-button class = "btn btn-secondary"@click = "dialogVisible = false;previewUrl = ''">取消</el-button>
<el-button class = "btn btn-primary" type="primary" @click="submitAvatar"></el-button>
</template>
</el-dialog>
<h3>{{ formData.username }}</h3>
<p class="preview-email">{{ formData.email }}</p>
<div class="preview-bio">
{{ formData.bio }}
</div>
</div>
<!-- 可爱装饰元素 -->
<div class="cute-decoration star-3"></div>
<div class="cute-decoration star-4"></div>
<div class="cute-decoration star-5"></div>
<div class="cute-decoration star-6"></div>
<div class="cute-decoration star-1"></div>
<div class="cute-decoration star-2"></div>
<div class="cute-decoration heart">💜</div>
<div class="cute-decoration cat">🐱</div>
<div class="cute-decoration cake">🥰🍰</div>
</div>
</div>
</div>
</template>
<style scoped>
.error-msg{
z-index: 1000;
height:50px;
width:30%;
position: absolute;
top:8%;
left:20%;
display: flex;
transition:2s;
}
/*错误信息提示*/
.fade-up-enter-active,
.fade-up-leave-active{
transition: all 0.4s ease;
}
.fade-up-enter-from,
.fade-up-leave-to{
opacity:0;
transform:translateY(10px);
}
.fade-up-enter-to,
.fade-up-leave-from{
opacity:1;
transform:translateY(0);
}
.hidden
{
display: none;
}
/* 主容器 */
.container {
position:absolute;
background-color:transparent;
left:100px;
top:2%;
width:94%;
height: 96%;
}
/* 侧边栏 */
/* 用户头像容器 */
.avatar-container {
display: flex;
justify-content: center;
margin: 20px 0 30px;
}
.avatar-wrapper{
position:relative;
width: 200px;
height:200px;
cursor:pointer;
border-radius:50%;
overflow:hidden;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.avatar-image{
width:100%;
height:100%;
object-fit:cover;
}
.avatar-hover-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.avatar-uploader {
display: flex;
justify-content: center;
align-items: center;
height: 150px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
}
.avatar-preview {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
}
/* 主内容区 */
.main-content {
flex: 1;
height:94%;
padding: 30px;
display: flex;
gap: 20px;
}
/* 左侧内容卡片 */
.profile-info-card {
flex: 2;
}
/* 右侧内容卡片 */
.profile-preview-card {
padding-top:90px;
flex: 1;
}
/* 分隔线 */
.divider {
height: 1px;
background-color: #e6e6fa;
margin: 25px 0;
border-radius: 1px;
}
/* 表单区 */
.form-section {
margin-bottom: 30px;
}
.section-title {
font-size: 2rem;
color: #9370DB;
margin-bottom: 20px;
font-weight: 500;
}
.form-group {
height:60px;
margin-bottom: 15px;
margin-left:10px;
display: flex;
align-items: center;
}
.form-label {
width: 95px;
font-size: 1.25rem;
color: #666;
text-align:left;
}
/* 输入框样式 */
.form-input {
flex: 1;
height:30px;
padding: 10px 15px;
border: 2px solid #e6e6fa;
border-radius: 25px;
outline: none;
transition: border-color 0.3s ease;
font-size: 1.2rem;
}
.form-input:focus {
border-color: #b19cd9;
}
/* 单选按钮样式 */
.radio-group {
transform:scale(1.2);
display: flex;
gap: 30px;
}
.radio-label {
display: flex;
align-items: center;
cursor: pointer;
}
.radio-input {
appearance: none;
-webkit-appearance: none;
width: 20px;
height: 20px;
border: 2px solid #e6e6fa;
border-radius: 50%;
margin-right: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.radio-input:checked {
border-color: #9370DB;
}
.radio-input:checked::before {
content: "";
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #9370DB;
}
/* 日期选择器 */
.date-input {
width: 100%;
padding: 10px 15px;
border: 2px solid #e6e6fa;
border-radius: 25px;
outline: none;
transition: border-color 0.3s ease;
background-color: #fff;
font-size:1.2rem;
}
.date-input:focus {
border-color: #b19cd9;
}
/* 按钮样式 */
/* 预览区样式 */
.preview-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.preview-avatar {
width: 150px;
height: 150px;
border-radius: 50%;
border: 5px solid #fff;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 10px;
}
.preview-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-email {
color: #666;
font-size: 1.2rem;
margin-bottom: 15px;
margin-top:0px;
}
.preview-bio {
width: 100%;
min-height: 100px;
padding: 15px;
border: 2px solid #e6e6fa;
border-radius: 15px;
background-color: #f9f7ff;
font-size: 1.2rem;
color: #666;
}
/* 密码修改区域样式 */
.password-inputs {
display: flex;
gap: 10px;
margin-right: 10px;
}
.password-inputs .form-input {
flex: 1;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.main-content {
flex-direction: column;
}
}
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.sidebar {
width: 100%;
padding: 10px;
}
.menu-item {
border-radius: 25px;
margin-right: 0;
padding: 10px 15px;
text-align: center;
}
.main-content {
padding: 15px;
}
.form-group {
flex-direction: column;
align-items: flex-start;
}
.form-label {
width: 100%;
margin-bottom: 5px;
}
.password-inputs {
flex-direction: column;
}
}
/* 可爱元素:气泡背景 */
.bubble {
position: absolute;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
z-index: -1;
animation: float 15s ease-in-out infinite;
}
.bubble-1 {
width: 100px;
height: 100px;
bottom: 50px;
left: 20px;
animation-delay: 0s;
}
.bubble-2 {
width: 60px;
height: 60px;
top: 100px;
left: 40px;
animation-delay: 2s;
}
.bubble-3 {
width: 80px;
height: 80px;
bottom: 200px;
left: 60px;
animation-delay: 5s;
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fadeIn 0.5s ease forwards;
}
.profile-preview-card {
animation-delay: 0.2s;
}
/* 提示文本样式 */
.form-hint {
font-size: 0.8rem;
color: #9370DB;
margin-top: 5px;
margin-left: 100px;
}
/* 可爱装饰元素 */
.cute-decoration {
position: absolute;
font-size: 1.5rem;
opacity: 0.3;
color: #9370DB;
pointer-events: none;
}
.star-1 {
top: 50px;
right: 20px;
}
.star-2 {
bottom: 100px;
right: 40px;
}
.star-3 {
top: 134px;
right: 239px;
}
.star-4 {
top: 70px;
right: 30px;
}
.star-5 {
top: 20px;
right: 100px;
}
.star-6 {
top: 20px;
right: 100px;
}
.star-7 {
top: 20px;
right: 100px;
}
.star-8 {
top: 20px;
right: 100px;
}
.heart {
bottom: 30px;
right: 30px;
}
</style>

@ -1,578 +0,0 @@
<template>
<div class="container">
<!-- 引用 HeaderBar.vue 组件 -->
<HeaderBar />
<!-- 主容器 -->
<div class="main-container">
<!-- 侧边栏 -->
<aside class="sidebar card">
<h2 class="sidebar-title">历史对话</h2>
<ul class="history-list">
<li class="history-item"
v-for="conversation in conversations"
:key="conversation.id"
@click="selectConversation(conversation)">
<span class="history-text">{{ conversation.title }}</span>
<div class="history-actions">
<button class="action-btn" title="编辑" @click.stop="editConversationTitle(conversation)">
<img src="@/assets/images/edit.png" alt="编辑" />
</button>
<button class="action-btn" title="删除" @click.stop="deleteConversation(conversation)">
<img src="@/assets/images/delete.png" alt="删除" />
</button>
</div>
</li>
</ul>
<!-- 新建对话按钮 -->
<button class="new-chat-btn" @click="createNewChat">
<span>新建对话</span>
</button>
</aside>
<!-- 聊天区域 -->
<main class="chat-area card">
<div class="chat-content">
<!-- 欢迎消息始终显示在最上方 -->
<div class="welcome-message">
<div class="bot-avatar">
<img src="@/assets/images/aiRobot.png" alt="机器人头像" />
</div>
<div class="welcome-text">我可以回答各种问题欢迎提问</div>
</div>
<!-- 对话消息 -->
<template v-if="currentConversation">
<div v-for="message in currentConversation.messages"
:key="message.id"
:class="message.isUser ? 'user-message' : 'bot-message'">
<template v-if="message.isUser">
<div class="message-text">{{ message.text }}</div>
<div class="user-avatar">
<img src="@/assets/images/aiRobot.png" alt="用户头像" />
</div>
</template>
<template v-else>
<div class="bot-avatar">
<img src="@/assets/images/aiRobot.png" alt="机器人头像" />
</div>
<div class="message-text">{{ message.text }}</div>
</template>
</div>
</template>
</div>
<!-- 输入区域 -->
<div class="input-area">
<div class="input-container">
<textarea class="message-input" placeholder="请输入您的问题..." v-model="message"></textarea>
<button class="send-button" @click="sendMessage"></button>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import HeaderBar from '@/components/HeaderBar.vue';
// HistoryItem
interface HistoryItem {
id: number; //
text: string; //
}
//
interface Message {
id: number;
text: string;
isUser: boolean; // true false
}
//
interface Conversation {
id: number;
title: string; // 20
messages: Message[]; //
timestamp: number; //
}
//
const historyItems = ref<HistoryItem[]>([
{ id: 1, text: '这是一个历史对话' },
{ id: 2, text: '这是另一个历史对话' },
]);
//
const message = ref('');
//
const chatMessages = ref<HistoryItem[]>([]);
//
const sendMessage = async () => {
const msg = message.value.trim();
if (!msg) return;
//
if (!currentConversation.value) {
currentConversation.value = {
id: Date.now(),
title: msg.slice(0, 20) + (msg.length > 20 ? '...' : ''),
messages: [],
timestamp: Date.now()
};
conversations.value.push(currentConversation.value);
}
//
const userMessage: Message = {
id: Date.now(),
text: msg,
isUser: true
};
currentConversation.value.messages.push(userMessage);
//
message.value = '';
//
try {
// TODO: AI
const botResponse = await mockBotResponse(msg);
const botMessage: Message = {
id: Date.now(),
text: botResponse,
isUser: false
};
currentConversation.value.messages.push(botMessage);
} catch (error) {
console.error('发送消息失败:', error);
}
};
//
const editItem = (item: HistoryItem) => {
const newText = prompt('编辑对话内容:', item.text);
if (newText !== null) {
item.text = newText.trim();
const chatItem = chatMessages.value.find((chat) => chat.id === item.id);
if (chatItem) {
chatItem.text = newText.trim(); //
}
}
};
//
const deleteItem = (item: HistoryItem) => {
historyItems.value = historyItems.value.filter((history) => history.id !== item.id);
chatMessages.value = chatMessages.value.filter((chat) => chat.id !== item.id); //
};
//
const conversations = ref<Conversation[]>([]);
//
const currentConversation = ref<Conversation | null>(null);
//
const selectConversation = (conversation: Conversation) => {
currentConversation.value = conversation;
};
//
const deleteConversation = (conversation: Conversation) => {
conversations.value = conversations.value.filter(c => c.id !== conversation.id);
if (currentConversation.value?.id === conversation.id) {
currentConversation.value = null;
}
};
//
const createNewChat = () => {
const newConversation: Conversation = {
id: Date.now(),
title: '新对话',
messages: [],
timestamp: Date.now()
};
conversations.value.push(newConversation);
currentConversation.value = newConversation;
};
//
const editConversationTitle = (conversation: Conversation) => {
const newTitle = prompt('编辑对话标题:', conversation.title);
if (newTitle !== null && newTitle.trim() !== '') {
conversation.title = newTitle.trim();
}
};
//
const mockBotResponse = async (msg: string): Promise<string> => {
return new Promise(resolve => {
setTimeout(() => {
resolve(`这是对 "${msg}" 的回复`);
}, 1000);
});
};
</script>
<style scoped>
/* 重置默认样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #f5f1ff 0%, #e8ddff 100%);
min-height: 100vh;
}
/* 主容器 */
.main-container {
display: flex;
height: 100%; /* 确保主容器的高度是100% */
}
.container {
position: absolute;
background-color: transparent;
left: 100px;
top: 2%;
width: 94%;
height: 100%;
}
/* 历史对话栏 */
.sidebar {
width: 300px;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
border: 2px solid rgba(133, 88, 207, 0.5);
padding: 20px;
margin-left: 150px;
overflow-y: auto;
height: 80%; /* 设置高度与 .chat-area 的高度一致 */
margin-top: 5%; /* 确保与 .chat-area 的顶部对齐 */
border-radius:10px;
transform: translateX(100px);
}
.sidebar-title {
font-size: 26px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
text-align: center;
}
.history-list {
list-style: none;
}
.history-item {
background: rgba(255, 255, 255, 0.8);
border: 2px solid rgba(133, 88, 207, 0.5);
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.3s;
}
.history-item:hover {
background: rgba(156, 136, 255, 0.1);
border-color: #9c88ff;
}
.history-text {
font-size: 18px;
color: #666;
flex: 1;
}
.history-actions {
display: flex;
gap: 8px;
}
.action-btn {
width: 24px;
height: 24px;
background: none;
border: none;
cursor: pointer;
border-radius: 4px;
transition: color 0.3s;
}
.action-btn:hover {
color: #9c88ff;
}
.new-chat-btn {
width: 100%;
padding: 12px 16px;
background: linear-gradient(45deg, #9c88ff, #ff6b9d);
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
text-align: center;
margin-top: 20px;
}
.new-chat-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(156, 136, 255, 0.3);
}
.new-chat-btn:active {
transform: translateY(0);
}
/* 聊天区域 */
.chat-area {
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.5);
border: 2px solid rgba(133, 88, 207, 0.5);
margin-top: 5%;
margin-left: 7%;
width: 60%;
height: 80%;
border-radius: 10px;
}
.chat-content {
flex: 1;
padding: 40px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.welcome-message {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(200, 180, 255, 0.3);
border-radius: 20px;
padding: 16px 24px;
box-shadow: 0 4px 20px rgba(156, 136, 255, 0.1);
max-width: 60%;
margin-bottom: 16px;
}
.bot-avatar {
width: 40px;
height: 40px;
background: transparent;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: white;
margin-right: 16px;
}
.bot-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.welcome-text {
font-size: 20px;
color: #333;
}
.user-message {
display: flex;
align-items: center;
justify-content: flex-end;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(200, 180, 255, 0.3);
border-radius: 20px;
padding: 16px 24px;
box-shadow: 0 4px 20px rgba(156, 136, 255, 0.1);
max-width: 60%;
margin-bottom: 16px;
align-self: flex-end;
}
.user-text {
font-size: 16px;
color: #333;
margin-right: 16px;
}
.user-avatar {
width: 40px;
height: 40px;
background: transparent;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.bot-message {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(200, 180, 255, 0.3);
border-radius: 20px;
padding: 16px 24px;
box-shadow: 0 4px 20px rgba(156, 136, 255, 0.1);
max-width: 60%;
margin-bottom: 16px;
align-self: flex-start;
}
.message-text {
font-size: 16px;
color: #333;
margin: 0 16px;
}
/* 输入区域 */
.input-area {
padding: 30px;
border-top: 1px solid rgba(200, 180, 255, 0.3);
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
height: 300px;
display: flex; /* 使用 Flexbox 布局 */
justify-content: center; /* 水平方向居中 */
align-items: center; /* 垂直方向居中 */
border-radius: 0 0 10px 10px; /* 设置下两个角为圆角,值为 20px */
}
.input-container {
display: flex;
width: 100%;
gap: 16px;
align-items: flex-end; /* 子元素在垂直方向底部对齐 */
position: relative; /* 设置相对定位以便按钮可以绝对定位 */
}
.message-input {
flex: 1;
height: 230px;
padding: 16px;
border: 1px solid rgba(200, 180, 255, 0.5);
border-radius: 12px;
resize: none;
font-size: 20px;
font-family: inherit;
outline: none;
background: rgba(255, 255, 255, 0.9);
transition: border-color 0.3s;
}
.message-input:focus {
border-color: #9c88ff;
}
.send-button {
position: absolute;
bottom: 20px;
right: 20px;
padding: 12px 24px;
background: linear-gradient(45deg, #9c88ff, #ff6b9d);
color: white;
border: none;
border-radius: 8px;
font-size: 20px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
white-space: nowrap;
}
.send-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(156, 136, 255, 0.3);
}
.send-button:active {
transform: translateY(0);
}
/* 响应式设计 */
@media (max-width: 768px) {
.sidebar {
width: 250px;
}
.search-bar {
max-width: 200px;
}
.nav-icons {
gap: 12px;
}
}
@media (max-width: 640px) {
.main-container {
flex-direction: column;
}
.sidebar {
width: 100%;
height: 150px;
border-right: none;
border-bottom: 1px solid rgba(200, 180, 255, 0.3);
}
.chat-area {
height: calc(100vh - 220px);
margin-top: 150px; /* 调整为150px以适应HeaderBar和侧边栏的高度 */
}
.search-bar {
display: none;
}
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: rgba(200, 180, 255, 0.1);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: rgba(156, 136, 255, 0.3);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(156, 136, 255, 0.5);
}
</style>

@ -1,312 +0,0 @@
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
interface Message {
id: number;
content: string;
time: string;
detail: string;
}
export default defineComponent({
setup() {
const messages = ref<Message[]>([
{ id: 1, content: "这是第一条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 10:30" },
{ id: 2, content: "这是第二条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 11:15" },
{ id: 3, content: "这是第三条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 12:00" },
{ id: 4, content: "这是第四条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 12:45" },
{ id: 5, content: "这是第五条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 13:30" },
{ id: 6, content: "这是第六条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 14:15" },
{ id: 7, content: "这是第七条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 15:00" },
//
]);
const currentPage = ref<number>(1);
const messagesPerPage = ref<number>(5);
const isModalOpen = ref<boolean>(false);
const selectedMessage = ref<Message | null>(null);
const totalPages = computed((): number => {
return Math.ceil(messages.value.length / messagesPerPage.value);
});
const currentPageMessages = computed((): Message[] => {
const startIndex = (currentPage.value - 1) * messagesPerPage.value;
const endIndex = startIndex + messagesPerPage.value;
return messages.value.slice(startIndex, endIndex);
});
const changePage = (pageNumber: number): void => {
if (pageNumber >= 1 && pageNumber <= totalPages.value) {
currentPage.value = pageNumber;
}
};
const openModal = (message : Message) :void => {
selectedMessage.value = message;
isModalOpen.value = true;
document.body.style.overflow="hidden";
}
const closeModal = () :void => {
selectedMessage.value = null;
isModalOpen.value = false;
document.body.style.overflow='';
}
return {
messages,
currentPage,
messagesPerPage,
totalPages,
currentPageMessages,
changePage,
isModalOpen,
selectedMessage,
openModal,
closeModal,
};
}
});
</script>
<template>
<div class="message-center">
<!-- 右侧主要内容区域 -->
<div class="main-content">
<!-- 顶部标题区域 -->
<div class="content-header">
<span class="header-title">评论回复</span>
</div>
<!-- 消息框区域 - 现在是垂直排列 -->
<div class="message-container">
<div v-for="(message, index) in currentPageMessages"
:key="index"
class="message-box"
@click="openModal(message)">
<div class="message-content">{{ message.content }}</div>
<div class="message-time">{{ message.time }}</div>
</div>
</div>
<!-- 分页控制区域 -->
<div class="pagination-controls" v-if="totalPages > 1">
<button
class="page-button"
:disabled="currentPage === 1"
@click="changePage(currentPage - 1)"
>
上一页
</button>
<span class="page-indicator">{{ currentPage }} / {{ totalPages }}</span>
<button
class="page-button"
:disabled="currentPage === totalPages"
@click="changePage(currentPage + 1)"
>
下一页
</button>
</div>
</div>
<div v-if="isModalOpen" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h3>消息详情</h3>
</div>
<div class="modal-body">
<p>{{ selectedMessage?.detail }}</p>
</div>
<div class="modal-footer">
<div class="message-time">{{ selectedMessage?.time }}</div>
<button class="confirm-button" @click="closeModal"></button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.message-center {
display: flex;
width: calc(100% - 100px); /* 减去侧边栏宽度 */
height: calc(100vh - 120px);
margin: 130px 20px 20px 350px; /* 左侧margin增加到120px为侧边栏留出空间 */
padding: 0;
border-radius: 10px;
}
/* 主要内容区域样式 */
.main-content {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
}
/* 顶部标题区域样式 */
.content-header {
height: 70px;
width:1267px;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
border: 2px solid rgba(133, 88, 207, 0.5);
border-radius: 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
padding: 0 20px;
}
.header-title {
font-size: 30px;
font-weight: bold;
color: #7e7e7e;
}
/* 消息框容器样式 - 修改为垂直排列 */
.message-container {
display: flex;
flex-direction: column; /* 改为垂直排列 */
gap: 15px; /* 消息框之间的间距 */
margin-bottom: 20px;
overflow-y: auto; /* 如果内容过多可以滚动 */
}
/* 单个消息框样式 */
.message-box {
width: 1275px; /* 宽度占满容器 */
min-height: 80px; /* 减小高度使垂直排列更紧凑 */
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(5px);
border: 1px solid rgba(133, 88, 207, 0.3);
border-radius: 8px;
padding: 15px;
display: flex;
flex-direction: column;
justify-content: space-between;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.message-box:hover{
transform: translateY(-3px);
box-shadow: 0 20px 10px rgba(0, 0, 0, 0.1);
}
.message-content{
font-size: 24px;
color:#333;
margin-bottom: 10px;
flex-grow: 1;
}
.message-time{
font-size: 18px;
color:#888;
text-align: right;
}
.pagination-controls{
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.page-button{
background: rgba(133, 88, 207, 0.7);
color: white;
border: none;
border-radius: 5px;
padding: 8px 15px;
cursor: pointer;
margin: 0 10px;
transition:background 0.2s;
position: relative;
right: 200px;
}
.page-button:hover:not(:disabled){
background: rgba(133, 88, 207, 0.9);
}
.page-button:disabled{
background: rgba(133, 88, 207, 0.3);
cursor: not-allowed;
}
.page-indicator{
font-size: 14px;
color: #666;
position: relative;
right: 200px;
}
.modal-overlay{
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content{
background: #fff;
width: 600px;
border-radius: 10px;
box-shadow: 0 5px 30px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
height: 800px;
font-size: 28px;
}
.modal-header{
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3{
margin: 0;
color: #333;
}
.modal-body{
padding: 20px;
overflow-y: visible;
flex-grow: 1;
line-height: 1.6;
}
.modal-footer{
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.confirm-button{
background: rgba(133, 88, 207, 0.7);
color: white;
border: none;
border-radius: 5px;
padding: 8px 20px;
cursor: pointer;
transition: background 0.2s;
}
.confirm-button:hover{
background: rgba(133, 88, 207, 0.9);
}
</style>

@ -1,350 +0,0 @@
<script setup lang="ts">
//
interface OverlayConfig {
id: string
dayIndex: number // 0=, 1=, ..., 6=
startLessonIndex: number // (0-based)
endLessonIndex: number // (0-based, )
title: string
courseInfo: {
name: string
time: string
location: string
teacher: string
}
}
//
interface OverlayStyle {
top: string
left: string
width: string
height: string
transform: string
}
import { ref, onMounted, reactive } from 'vue'
const table = ref<HTMLElement | null>(null)
const showModal = ref(false)
const currentModalInfo = ref<OverlayConfig | null>(null)
//
const overlayConfigs: OverlayConfig[] = [
{
id: 'overlay-1',
dayIndex: 0, //
startLessonIndex: 0, // 1
endLessonIndex: 1, // 2
title: '专题研讨',
courseInfo: {
name: '专题研讨',
time: '08:00 - 09:40',
location: '教学楼A301',
teacher: '张老师'
}
},
{
id: 'overlay-2',
dayIndex: 1, //
startLessonIndex: 2, // 3
endLessonIndex: 3, // 4
title: '高等数学',
courseInfo: {
name: '高等数学',
time: '10:00 - 11:40',
location: '教学楼B205',
teacher: '李老师'
}
},
{
id: 'overlay-3',
dayIndex: 2, //
startLessonIndex: 5, // 6
endLessonIndex: 6, // 7
title: '英语听力',
courseInfo: {
name: '英语听力',
time: '14:00 - 15:40',
location: '语音室101',
teacher: '王老师'
}
},
{
id: 'overlay-4',
dayIndex: 3, //
startLessonIndex: 7, // 8
endLessonIndex: 8, // 9
title: '计算机基础',
courseInfo: {
name: '计算机基础',
time: '16:00 - 17:40',
location: '机房302',
teacher: '赵老师'
}
},
{
id: 'overlay-5',
dayIndex: 4, //
startLessonIndex: 0, // 1
endLessonIndex: 2, // 3
title: '物理实验',
courseInfo: {
name: '物理实验',
time: '08:00 - 10:30',
location: '实验楼201',
teacher: '陈老师'
}
}
]
//
const overlayStyles = reactive<Record<string, OverlayStyle>>({})
const hoveredOverlays = reactive<Record<string, boolean>>({})
const currentMonth = new Date().getMonth() + 1
const weekdays = ['Sun','Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const lessons = Array.from({length: 14}, (_, i) => `${i + 1}节课`)
const Data = Array(14).fill('').map(() => Array(7).fill(''))
onMounted(() => {
calculateAllOverlayPositions()
window.addEventListener('resize', calculateAllOverlayPositions)
})
/**
* 计算所有覆盖层的位置
*/
function calculateAllOverlayPositions() {
overlayConfigs.forEach(config => {
calculateOverlayPosition(config)
})
}
/**
* 计算单个覆盖层的位置
*/
function calculateOverlayPosition(config: OverlayConfig) {
if (!table.value) return
const tbody = table.value.querySelector('tbody')
if (!tbody) return
const rows = tbody.querySelectorAll('tr')
if (rows.length <= config.endLessonIndex) return
//
const startCell = rows[config.startLessonIndex].querySelectorAll('td')[config.dayIndex]
const endCell = rows[config.endLessonIndex].querySelectorAll('td')[config.dayIndex]
if (!startCell || !endCell) return
const startRect = startCell.getBoundingClientRect()
const endRect = endCell.getBoundingClientRect()
const tableRect = table.value.getBoundingClientRect()
//
const top = startRect.top - tableRect.top
const left = startRect.left - tableRect.left
const width = startRect.width
const height = (endRect.bottom - startRect.top)
//
overlayStyles[config.id] = {
top: `${top}px`,
left: `${left}px`,
width: `${width}px`,
height: `${height}px`,
transform: hoveredOverlays[config.id] ? 'scale(0.8)' : 'scale(1)'
}
}
function handleMouseEnter(overlayId: string) {
hoveredOverlays[overlayId] = true
if (overlayStyles[overlayId]) {
overlayStyles[overlayId].transform = 'scale(0.8)'
}
}
function handleMouseLeave(overlayId: string) {
hoveredOverlays[overlayId] = false
if (overlayStyles[overlayId]) {
overlayStyles[overlayId].transform = 'scale(1)'
}
}
function openModal(config: OverlayConfig) {
currentModalInfo.value = config
showModal.value = true
}
function closeModal(event?: MouseEvent) {
if (event && event.target !== event.currentTarget) return
showModal.value = false
currentModalInfo.value = null
}
</script>
<template>
<div class="flow-container">
<table ref="table" class="timetable">
<thead>
<tr>
<th class="month-cell">{{ currentMonth }}</th>
<th v-for="day in weekdays" :key="day">{{day}}</th>
</tr>
</thead>
<tbody>
<tr v-for="(lesson,lessonIndex) in lessons" :key="lessonIndex">
<th class="lesson-cell">{{lesson}}</th>
<td
v-for="(_,dayIndex) in weekdays"
:key="dayIndex"
contenteditable="false"
>
{{ Data[lessonIndex][dayIndex] }}
</td>
</tr>
</tbody>
<div
v-for="config in overlayConfigs"
:key="config.id"
:ref="config.id"
class="overlay"
:class="{ 'hovered': hoveredOverlays[config.id] }"
:style="overlayStyles[config.id] || {}"
@mouseenter="handleMouseEnter(config.id)"
@mouseleave="handleMouseLeave(config.id)"
@click="openModal(config)">
{{ config.title }}
</div>
</table>
<!-- 模态框 -->
<div v-if="showModal && currentModalInfo" class="modal-backdrop" @click="closeModal">
<div class="modal-content" @click.stop>
<h3>{{ currentModalInfo.courseInfo.name }}</h3>
<ul>
<li>课程名称: {{ currentModalInfo.courseInfo.name }}</li>
<li>上课时间: {{ currentModalInfo.courseInfo.time }}</li>
<li>上课地点: {{ currentModalInfo.courseInfo.location }}</li>
<li>授课老师: {{ currentModalInfo.courseInfo.teacher }}</li>
</ul>
<button @click="closeModal"></button>
</div>
</div>
</div>
</template>
<style scoped>
.timetable-container
{
width: 100%;
overflow: hidden;
}
.timetable
{
margin-top:100px;
margin-left:10px;
position:relative;
width: 90%;
height:80%;
table-layout: fixed;
border-collapse: collapse;
}
.month-cell
{
background-color: mediumpurple;
}
.timetable tr:first-child th:not(.month-cell)
{
background-color: pink;
}
.lesson-cell
{
background-color: pink;
}
.timetable td
{
border: 1px solid #e0e0e0;
padding: 4px;
height: 28px;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background-color: white;
}
.overlay {
position: absolute;
background-color: rgba(0, 123, 255, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
color: white;
font-weight: bold;
transform-origin: center;
transition: transform 0.3s ease;
box-sizing: border-box;
}
.modal-backdrop
{
position:fixed;
top:0;
left:0;
width:100vw;
height:100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index:100;
}
.modal-content
{
background-color: white;
padding: 25px;
border-radius: 8px;
box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.2);
max-width: 400px;
width: 90%;
text-align: center;
color: #333;
z-index: 101;
}
.modal-content h3
{
margin-top: 0;
color: #007bff;
}
.modal-content ul
{
list-style: none;
padding: 0;
text-align: left;
margin-top: 15px;
margin-bottom: 20px;
}
.modal-content ul li
{
margin-bottom: 8px;
}
.modal-content button
{
padding: 10px 20px;
background-color: #007bff;
color:white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s ease;
}
.modal-content button:hover
{
background-color: #0056b3;
}
</style>

@ -1,402 +0,0 @@
<template>
<div class="message-center">
<!-- 右侧主要内容区域 -->
<div class="main-content">
<!-- 顶部标题区域 -->
<div class="content-header">
<span class="header-title">私信消息</span>
</div>
<!-- 下方内容区域 -->
<div class="content-body">
<!-- 中间消息列表使用 AiManager 的样式 -->
<aside class="sidebar">
<h2 class="sidebar-title">近期消息</h2>
<ul class="history-list">
<li class="history-item"
v-for="conversation in conversations"
:key="conversation.id"
@click="selectConversation(conversation)">
<span class="history-text">{{ conversation.title }}</span>
</li>
</ul>
</aside>
<!-- 右侧聊天区域使用 AiManager 的样式 -->
<main class="chat-area">
<!-- 添加用户信息区域 -->
<div class="chat-header">
<div class="user-info">
<div class="chat-avatar">
<img src="@/assets/images/aiRobot.png" alt="用户头像" />
</div>
<span class="chat-username">{{ currentConversation?.title || '选择一个聊天' }}</span>
</div>
</div>
<div class="chat-content">
<!-- 对话消息 -->
<template v-if="currentConversation">
<div v-for="message in currentConversation.messages"
:key="message.id"
:class="message.isUser ? 'user-message' : 'bot-message'">
<template v-if="message.isUser">
<div class="message-text">{{ message.text }}</div>
<div class="user-avatar">
<img src="@/assets/images/aiRobot.png" alt="用户头像" />
</div>
</template>
<template v-else>
<div class="bot-avatar">
<img src="@/assets/images/aiRobot.png" alt="对方头像" />
</div>
<div class="message-text">{{ message.text }}</div>
</template>
</div>
</template>
</div>
<!-- 输入区域 -->
<div class="input-area">
<div class="input-container">
<textarea
class="message-input"
placeholder="输入消息..."
v-model="message"
></textarea>
<button class="send-button" @click="sendMessage"></button>
</div>
</div>
</main>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
//
interface Message {
id: number;
text: string;
isUser: boolean;
timestamp: number;
}
//
interface Conversation {
id: number;
title: string;
messages: Message[];
timestamp: number;
}
//
const conversations = ref<Conversation[]>([
{
id: 1,
title: '用户A',
messages: [
{
id: 1,
text: '你好!',
isUser: false,
timestamp: Date.now() - 1000
}
],
timestamp: Date.now()
}
]);
//
const currentConversation = ref<Conversation | null>(null);
//
const message = ref('');
//
const selectConversation = (conversation: Conversation) => {
currentConversation.value = conversation;
};
//
const sendMessage = () => {
if (!currentConversation.value || !message.value.trim()) return;
const newMessage: Message = {
id: Date.now(),
text: message.value.trim(),
isUser: true,
timestamp: Date.now()
};
currentConversation.value.messages.push(newMessage);
message.value = '';
};
</script>
<style scoped>
/* 修改消息中心的样式 */
.message-center {
display: flex;
width: calc(100% - 100px); /* 减去侧边栏宽度 */
height: calc(100vh - 120px);
margin: 130px 20px 20px 350px; /* 左侧margin增加到120px为侧边栏留出空间 */
padding: 0;
border-radius: 10px;
}
/* 主要内容区域样式 */
.main-content {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
}
/* 顶部标题区域样式 */
.content-header {
height: 70px;
width:1267px;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
border: 2px solid rgba(133, 88, 207, 0.5);
border-radius: 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
padding: 0 20px;
}
.header-title {
font-size: 30px;
font-weight: bold;
color: #7e7e7e;
}
/* 下方内容区域样式 */
.content-body {
display: flex;
flex: 1;
gap: 0; /* 移除间距 */
}
/* 消息列表样式 */
.sidebar {
width: 15%; /* 调整为百分比宽度 */
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
border: 2px solid rgba(133, 88, 207, 0.5);
border-radius: 10px 0 0 10px; /* 修改右边圆角为0 */
margin-right: 0; /* 移除右侧间距 */
height: 93.5%; /* 减去顶部区域的高度和间距 */
}
.sidebar-title {
padding: 13px;
font-size: 26px;
border-bottom: 1px solid rgba(133, 88, 207, 0.2);
color: #7e7e7e;
text-align: center;
}
.history-list {
list-style: none;
padding: 0;
margin: 0;
}
.history-item {
padding: 15px 20px;
cursor: pointer;
border-bottom: 1px solid rgba(133, 88, 207, 0.2);
transition: all 0.3s;
}
.history-item:hover {
background: rgba(156, 136, 255, 0.1);
}
.history-text {
color: #333;
font-size: 22px;
}
/* 聊天区域样式 */
.chat-area {
width: 60%; /* 设置固定宽度比例 */
flex: none; /* 移除 flex: 1避免自动扩展 */
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
border: 2px solid rgba(133, 88, 207, 0.5);
border-radius: 0 10px 10px 0; /* 修改左边圆角为0 */
border-left: none; /* 移除左边框 */
height: 93.5%; /* 减去顶部区域的高度和间距 */
}
/* 聊天区域顶部用户信息样式 */
.chat-header {
padding: 15px 20px;
border-bottom: 1px solid rgba(133, 88, 207, 0.2);
background: rgba(255, 255, 255, 0.7);
border-radius: 0 10px 0 0;
height: 56px;
display: flex;
justify-content: center; /* 添加水平居中 */
align-items: center; /* 添加垂直居中 */
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
justify-content: center; /* 添加水平居中 */
}
.chat-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
.chat-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.chat-username {
font-size: 22px;
font-weight: 500;
color: #333;
text-align: center; /* 文字居中 */
}
.chat-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.user-message {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.bot-message {
display: flex;
justify-content: flex-start;
margin-bottom: 10px;
font-size: 20px;
}
.message-text {
max-width: 70%;
padding: 12px 16px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.9);
margin: 0 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
font-size: 20px;
}
.user-message .message-text {
background: rgba(156, 136, 255, 0.9);
color: white;
}
.bot-avatar,
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
}
.input-area {
padding: 20px; /* 减小内边距 */
border-top: 1px solid rgba(200, 180, 255, 0.3);
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
height: 200px; /* 从300px调整为200px */
display: flex;
justify-content: center;
align-items: center;
border-radius: 0 0 10px 10px;
}
.input-container {
display: flex;
width: 100%;
gap: 16px;
align-items: flex-end;
position: relative;
}
.message-input {
flex: 1;
height: 150px; /* 从230px调整为150px保持比例 */
padding: 16px;
border: 1px solid rgba(200, 180, 255, 0.5);
border-radius: 12px;
resize: none;
font-size: 22px; /* 稍微调小字体 */
font-family: inherit;
outline: none;
background: rgba(255, 255, 255, 0.9);
transition: border-color 0.3s;
}
.message-input:focus {
border-color: #9c88ff;
}
.send-button {
position: absolute;
bottom: 15px; /* 从20px调整为15px */
right: 15px; /* 从20px调整为15px */
padding: 12px 24px;
background: linear-gradient(45deg, #9c88ff, #ff6b9d);
color: white;
border: none;
border-radius: 8px;
font-size: 20px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
white-space: nowrap;
}
.send-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(156, 136, 255, 0.3);
}
.send-button:active {
transform: translateY(0);
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: rgba(200, 180, 255, 0.1);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: rgba(156, 136, 255, 0.3);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(156, 136, 255, 0.5);
}
</style>

@ -1,161 +0,0 @@
<template>
<div class = "flow-container">
<div class="forum-home">
<!-- 左侧主内容区 -->
<div class="left-section card">
<!-- 上半部分 -->
<div class="top-section">
<div class="carousel-wrapper">
<Carousel />
</div>
<div class="hot-topic-wrapper">
<h2>📍 今日热点</h2>
<HotTopic
v-for="(item, index) in hotTopics"
:key="index"
:title="item.title"
:link="item.link"
/>
</div>
</div>
<el-divider content-position="right">欢迎来到UniLife</el-divider>
<!-- 下半部分帖子 -->
<div class="posts-section">
<h2>帖子</h2>
<PostCard
v-for="(post, index) in posts"
:key="index"
:post="post"
/>
</div>
</div>
<!-- 右侧今日行程 -->
<div class="right-section">
<h2>📅 今日行程</h2>
<ScheduleCard />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Carousel from '@/components/Carousel.vue'
import PostCard from '@/components/PostCard.vue'
import HotTopic from '@/components/HotTopic.vue'
import ScheduleCard from '@/components/ScheduleCard.vue'
const hotTopics = [
{ title: '这是一个标题,一个热门帖子的标题', link: '/post/1' },
{ title: '这是一个标题,一个热门帖子的标题', link: '/post/2' },
{ title: '这是一个标题,一个热门帖子的标题', link: '/post/3' }
]
const posts = [
{
id: 1,
title: '蚂蚁金服设计平台简介',
tags: ['Ant Design', '设计语言', '蚂蚁金服'],
excerpt: '段落示意:这是帖子的部分具体内容……',
link: '/post/1'
},
{
id: 2,
title: '蚂蚁金服设计平台简介',
tags: ['Ant Design', '设计语言', '蚂蚁金服'],
excerpt: '段落示意:这是帖子的部分具体内容……',
link: '/post/2'
},
{
id: 3,
title: '蚂蚁金服设计平台简介',
tags: ['Ant Design', '设计语言', '蚂蚁金服'],
excerpt: '段落示意:这是帖子的部分具体内容……',
link: '/post/3'
},
{
id: 4,
title: '蚂蚁金服设计平台简介',
tags: ['Ant Design', '设计语言', '蚂蚁金服'],
excerpt: '段落示意:这是帖子的部分具体内容……',
link: '/post/4'
},
{
id: 5,
title: '蚂蚁金服设计平台简介',
tags: ['Ant Design', '设计语言', '蚂蚁金服'],
excerpt: '段落示意:这是帖子的部分具体内容……',
link: '/post/5'
},
]
</script>
<style scoped lang="scss">
.forum-home {
display: flex;
width:92%;
gap: 40px; // 🔧
.left-section {
flex: 3;
display: flex;
flex-direction: column;
margin-right:10%;
gap: 30px; // 🔧
background: linear-gradient(to bottom right, #f7f1ff, #ffffff);
}
.top-section {
display: flex;
gap: 24px; // 🔧
height: 320px;
background-color:transparent;
.carousel-wrapper {
flex: 2;
border-radius: 8px;
overflow: hidden;
}
.hot-topic-wrapper {
flex: 1;
padding: 16px;
background-color: #fef6ff;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
gap: 12px;
}
}
.posts-section {
display: flex;
flex-direction: column;
gap: 20px;
h2 {
margin-bottom: 12px;
}
}
.right-section {
flex: 1;
position:fixed;
margin-left:77%;
height: 830px;
min-width: 350px;
padding: 16px;
background-color: #f9f7ff;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
background: linear-gradient(to bottom right, #f7f1ff, #ffffff);
h2 {
margin-bottom: 16px;
}
}
}
</style>

@ -1,312 +0,0 @@
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
interface Message {
id: number;
content: string;
time: string;
detail: string;
}
export default defineComponent({
setup() {
const messages = ref<Message[]>([
{ id: 1, content: "这是第一条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 10:30" },
{ id: 2, content: "这是第二条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 11:15" },
{ id: 3, content: "这是第三条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 12:00" },
{ id: 4, content: "这是第四条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 12:45" },
{ id: 5, content: "这是第五条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 13:30" },
{ id: 6, content: "这是第六条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 14:15" },
{ id: 7, content: "这是第七条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 15:00" },
//
]);
const currentPage = ref<number>(1);
const messagesPerPage = ref<number>(5);
const isModalOpen = ref<boolean>(false);
const selectedMessage = ref<Message | null>(null);
const totalPages = computed((): number => {
return Math.ceil(messages.value.length / messagesPerPage.value);
});
const currentPageMessages = computed((): Message[] => {
const startIndex = (currentPage.value - 1) * messagesPerPage.value;
const endIndex = startIndex + messagesPerPage.value;
return messages.value.slice(startIndex, endIndex);
});
const changePage = (pageNumber: number): void => {
if (pageNumber >= 1 && pageNumber <= totalPages.value) {
currentPage.value = pageNumber;
}
};
const openModal = (message : Message) :void => {
selectedMessage.value = message;
isModalOpen.value = true;
document.body.style.overflow="hidden";
}
const closeModal = () :void => {
selectedMessage.value = null;
isModalOpen.value = false;
document.body.style.overflow='';
}
return {
messages,
currentPage,
messagesPerPage,
totalPages,
currentPageMessages,
changePage,
isModalOpen,
selectedMessage,
openModal,
closeModal,
};
}
});
</script>
<template>
<div class="message-center">
<!-- 右侧主要内容区域 -->
<div class="main-content">
<!-- 顶部标题区域 -->
<div class="content-header">
<span class="header-title">评论回复</span>
</div>
<!-- 消息框区域 - 现在是垂直排列 -->
<div class="message-container">
<div v-for="(message, index) in currentPageMessages"
:key="index"
class="message-box"
@click="openModal(message)">
<div class="message-content">{{ message.content }}</div>
<div class="message-time">{{ message.time }}</div>
</div>
</div>
<!-- 分页控制区域 -->
<div class="pagination-controls" v-if="totalPages > 1">
<button
class="page-button"
:disabled="currentPage === 1"
@click="changePage(currentPage - 1)"
>
上一页
</button>
<span class="page-indicator">{{ currentPage }} / {{ totalPages }}</span>
<button
class="page-button"
:disabled="currentPage === totalPages"
@click="changePage(currentPage + 1)"
>
下一页
</button>
</div>
</div>
<div v-if="isModalOpen" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h3>消息详情</h3>
</div>
<div class="modal-body">
<p>{{ selectedMessage?.detail }}</p>
</div>
<div class="modal-footer">
<div class="message-time">{{ selectedMessage?.time }}</div>
<button class="confirm-button" @click="closeModal"></button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.message-center {
display: flex;
width: calc(100% - 100px); /* 减去侧边栏宽度 */
height: calc(100vh - 120px);
margin: 130px 20px 20px 350px; /* 左侧margin增加到120px为侧边栏留出空间 */
padding: 0;
border-radius: 10px;
}
/* 主要内容区域样式 */
.main-content {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
}
/* 顶部标题区域样式 */
.content-header {
height: 70px;
width:1267px;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
border: 2px solid rgba(133, 88, 207, 0.5);
border-radius: 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
padding: 0 20px;
}
.header-title {
font-size: 30px;
font-weight: bold;
color: #7e7e7e;
}
/* 消息框容器样式 - 修改为垂直排列 */
.message-container {
display: flex;
flex-direction: column; /* 改为垂直排列 */
gap: 15px; /* 消息框之间的间距 */
margin-bottom: 20px;
overflow-y: auto; /* 如果内容过多可以滚动 */
}
/* 单个消息框样式 */
.message-box {
width: 1275px; /* 宽度占满容器 */
min-height: 80px; /* 减小高度使垂直排列更紧凑 */
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(5px);
border: 1px solid rgba(133, 88, 207, 0.3);
border-radius: 8px;
padding: 15px;
display: flex;
flex-direction: column;
justify-content: space-between;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.message-box:hover{
transform: translateY(-3px);
box-shadow: 0 20px 10px rgba(0, 0, 0, 0.1);
}
.message-content{
font-size: 24px;
color:#333;
margin-bottom: 10px;
flex-grow: 1;
}
.message-time{
font-size: 18px;
color:#888;
text-align: right;
}
.pagination-controls{
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.page-button{
background: rgba(133, 88, 207, 0.7);
color: white;
border: none;
border-radius: 5px;
padding: 8px 15px;
cursor: pointer;
margin: 0 10px;
transition:background 0.2s;
position: relative;
right: 200px;
}
.page-button:hover:not(:disabled){
background: rgba(133, 88, 207, 0.9);
}
.page-button:disabled{
background: rgba(133, 88, 207, 0.3);
cursor: not-allowed;
}
.page-indicator{
font-size: 14px;
color: #666;
position: relative;
right: 200px;
}
.modal-overlay{
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content{
background: #fff;
width: 600px;
border-radius: 10px;
box-shadow: 0 5px 30px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
height: 800px;
font-size: 28px;
}
.modal-header{
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3{
margin: 0;
color: #333;
}
.modal-body{
padding: 20px;
overflow-y: visible;
flex-grow: 1;
line-height: 1.6;
}
.modal-footer{
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.confirm-button{
background: rgba(133, 88, 207, 0.7);
color: white;
border: none;
border-radius: 5px;
padding: 8px 20px;
cursor: pointer;
transition: background 0.2s;
}
.confirm-button:hover{
background: rgba(133, 88, 207, 0.9);
}
</style>

@ -1,299 +0,0 @@
<script set lang="ts">
import { defineComponent } from 'vue';
import { computed } from 'vue';
interface UserInfo {
avatar: string;
nickname: string;
gender: 'male' | 'female';
followers: number;
following: number;
postsCount: number;
}
interface Post {
id: number;
title: string;
category: string;
views: number;
}
interface Course {
id: number;
time: string;
name: string;
location: string;
}
interface Assignment {
id: number;
title: string;
course: string;
dueDate: string;
}
export default defineComponent({
name:'Home',
setup() {
//
const userInfo: UserInfo = {
avatar: '@/assets/images/默认头像.jpg',
nickname: '学习小能手',
gender: 'male',
followers: 128,
following: 56,
postsCount: 32
};
const posts: Post[] = [
{ id: 1, title: '高数复习笔记', category: '学习资料', views: 356 },
{ id: 2, title: '英语作文模板分享', category: '学习交流', views: 234 }
];
const schedule: Course[] = [
{ id: 1, time: '周一 8:00', name: '高等数学', location: '教201' },
{ id: 2, time: '周三 10:00', name: '大学英语', location: '教305' },
{ id: 3, time: '周四 10:00', name: '大学物理', location: '教305' }
];
const assignments: Assignment[] = [
{ id: 1, title: '线性代数作业', course: '数学', dueDate: '2024-03-20' },
{ id: 2, title: '实验报告', course: '物理', dueDate: '2024-03-22' }
];
function getTodayWeekday(): string {
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const today = new Date();
return days[today.getDay()];
}
const todayWeekday = getTodayWeekday();
const todaySchedule = computed(() => {
return schedule.filter(course => course.time.startsWith(todayWeekday));
});
return {
userInfo,
posts,
schedule,
assignments,
todaySchedule
};
}
});
</script>
<template>
<div class="profile-container">
<!-- 用户信息头部 -->
<div class="user-header">
<img :src="userInfo.avatar" class="user-avatar" alt="头像">
<div class="user-info">
<h2 class="username">
{{ userInfo.nickname }}
<span class="gender-icon" :class="userInfo.gender"></span>
</h2>
<div class="stats">
<div class="stat-item">
<span class="stat-number">{{ userInfo.followers }}</span>
<span class="stat-label">粉丝</span>
</div>
<div class="stat-item">
<span class="stat-number">{{ userInfo.following }}</span>
<span class="stat-label">关注</span>
</div>
<div class="stat-item">
<span class="stat-number">{{ userInfo.postsCount }}</span>
<span class="stat-label">帖子</span>
</div>
</div>
</div>
</div>
<!-- 内容区域 -->
<div class="content-wrapper">
<!-- 帖子区域 -->
<div class="posts-section scrollable">
<h3 class="section-title">发布的帖子</h3>
<div v-for="post in posts" :key="post.id" class="post-item">
<h4 class="post-title">{{ post.title }}</h4>
<div class="post-meta">
<span class="post-category">{{ post.category }}</span>
<span class="post-views">浏览: {{ post.views }}</span>
</div>
</div>
</div>
<!-- 右侧侧边栏 -->
<div class="sidebar">
<!-- 课表区域 -->
<div class="schedule-section scrollable">
<h3 class="section-title">本周课表</h3>
<div v-for="course in todaySchedule" :key="course.id" class="schedule-item">
<div class="course-time">{{ course.time }}</div>
<div class="course-info">
<div class="course-name">{{ course.name }}</div>
<div class="course-location">{{ course.location }}</div>
</div>
</div>
</div>
<!-- 学习计划区域 -->
<div class="assignments-section scrollable">
<h3 class="section-title">待做作业</h3>
<div v-for="assignment in assignments" :key="assignment.id" class="assignment-item">
<div class="assignment-title">{{ assignment.title }}</div>
<div class="assignment-course">{{ assignment.course }}</div>
<div class="assignment-due">截止: {{ assignment.dueDate }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.profile-container {
width: 90vw; /* 宽度自适应,保证在大屏幕拉伸 */
max-width: 2000px; /* 最大宽度限制宽屏幕下显示 */
margin: 20px 0;
padding: 20px;
background: #f6f6f6; /* 白底 */
border-radius: 8px; /* 圆角,增强视觉 */
box-shadow: 0 0 10px rgba(0,0,0,0.1);
font-size: 20px;
}
.user-header {
display: flex;
align-items: center;
margin-bottom: 20px;
margin-top:40px;
}
.user-avatar {
width: 100px;
height: 60px;
border-radius: 50%;
margin-right: 30px;
}
.user-info h2 {
margin: 0 0 8px 0;
}
.gender-icon {
display: inline-block;
width: 20px;
height: 20px;
margin-left: 8px;
background-size: contain;
}
/*
.gender-icon.male {
background-image: url('male-icon.svg');
}
.gender-icon.female {
background-image: url('female-icon.svg');
}*/
.stats {
display: flex;
gap: 30px;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 30px;
font-weight: bold;
display: block;
}
.stat-label {
color: #666;
}
.content-wrapper {
display: grid;
grid-template-columns: 40% 25% 25%;
gap: 4%;
height: 800px;
width: 120%; /* 宽度撑满父容器 */
}
.scrollable {
overflow-y: auto;
height: 100%;
padding-right: 10px;
}
/* 帖子区域样式 */
.posts-section {
background: #f0eaf2;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.post-item {
padding: 12px;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
}
.post-title {
margin: 0 0 6px 0;
font-size: 24px;
}
.post-meta {
display: flex;
justify-content: space-between;
color: #666;
font-size: 18px;
}
/* 侧边栏公共样式 */
.sidebar {
display: flex;
flex-direction: column;
gap: 60px;
}
/* 课表样式 */
.schedule-item {
display: flex;
padding: 20px;
background: #f0eaf2;
margin-bottom: 8px;
border-radius: 6px;
}
.course-time {
width: 70px;
color: #666;
}
.course-name {
font-weight: 500;
}
/* 作业样式 */
.assignment-item {
padding: 10px;
background: #f0eaf2;
margin-bottom: 8px;
border-radius: 6px;
}
.assignment-due {
color: #e67e22;
font-size: 18px;
}
.section-title {
margin: 0 0 15px 0;
padding-bottom: 8px;
border-bottom: 2px solid #eee;
font-size: 30px;
}
</style>

@ -1,107 +0,0 @@
<template>
<div class = "flow-container">
<div class = "post-detail-page">
<!-- 左侧分类 包含分类 -->
<SidebarCategory class="sidebar card" />
<!-- 中间内容 帖子的具体内容-->
<PostContent :post="post" class="content card" />
<!-- 右侧作者信息 -->
<AuthorInfo class="author-info card" :author="post.author" />
</div>
</div>
</template>
<script setup lang = "ts">
import SidebarCategory from '@/components/PostDetailPage/SidebarCategory.vue'
import PostContent from '@/components/PostDetailPage/PostContent.vue'
import AuthorInfo from '@/components/PostDetailPage/AuthorInfor.vue'
import { useRoute } from 'vue-router'
import {ref, onMounted} from 'vue'
import Avatar from '@/assets/images/默认头像.jpg'
const route = useRoute()
const postId = ref<number>(parseInt(route.params.id as string))
const post = ref({
id: postId.value,
title: '帖子标题示例',
content: `
# 欢迎来到论坛
这是一个使用 **Vue3** + **Vite** + **TypeScript** 搭建的论坛系统
## 功能清单
- 支持 Markdown 渲染
- 高亮代码块
- 响应式布局
- 用户评论系统
## 示例代码
\`\`\`ts
function greet(name: string): string {
return \`Hello, \${name}!\`
}
console.log(greet('Vue'))
\`\`\`
## 引用与链接
> 学习不是人生的全部但如果连学习都掌握不了你还能做什么
请访问 [Vue 官方文档](https://vuejs.org)
## 图片测试
![Vue Logo](https://vuejs.org/images/logo.png)
`,
author: {
id: 1,
name: '张三',
avatar: Avatar,
bio: '一名热爱分享的大学生'
},
tags: ['Vue3', '论坛开发', '学习笔记']
})
//
onMounted(() => {
// TODO: fetch(`/api/post/${postId.value}`)
})
</script>
<style scoped lang="scss">
.post-detail-page {
display:flex;
gap:16px;
.sidebar{
width:15%;
position:sticky;
height: fit-content;
border-radius: 8px;
padding: 16px;
flex-shrink: 0;
}
.content{
width: 60%;
padding: 16px;
border-radius: 8px;
}
.author-info{
width: 20%;
position:sticky;
flex-shrink:0;
height:fit-content;
border-radius: 8px;
padding: 16px;
}
}
</style>

@ -1,286 +0,0 @@
<template>
<div class = "flow-container">
<div class="post-editor">
<!-- 顶部工具栏 -->
<div class="toolbar">
<router-link to = '/personal/postManager'>
<el-button :icon="ArrowLeft" circle></el-button>
</router-link>
<el-select v-model="selectedCategory" placeholder="选择分区" class="category-select" size="large">
<el-option
v-for="item in categories"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<div class="icon-group">
<el-button :icon="Document " circle size="large"></el-button>
<el-button :icon="Picture" circle size="large" @click="handlePicture"></el-button>
<el-button :icon="ArrowLeft" circle size="large"></el-button>
<el-button :icon="ArrowRight" circle size="large"></el-button>
</div>
<div class="action-buttons">
<button class = "btn-primary btn">保存草稿</button>
<button class = "btn-secondary btn">定时发布</button>
<button class = "btn-primary btn">发布帖子</button>
</div>
</div>
</div>
<!-- 图片上传对话框 -->
<el-dialog v-model="uploadPictureDialog" title="上传图片" :show-close="false">
<el-upload
:auto-upload="false"
:show-file-list="false"
:on-change="handleImageUpload"
accept="image/*"
class="upload-dialog"
>
<img v-if="previewUrl" :src="previewUrl" class="avatar-preview"/>
<el-icon size = 30px v-else><Plus/></el-icon>
</el-upload>
<template #footer>
<button class = "btn btn-primary" @click="uploadPictureDialog = false ,previewUrl = ''">确定</button>
</template>
</el-dialog>
<!-- 编辑器主体区域 -->
<div class="editor-body">
<!-- 左侧Markdown编辑框 -->
<div class="editor-pane card">
<el-input
v-model="title"
placeholder="请输入文章标题"
class="editor-title"
/>
<el-input
id="markdown-editor"
v-model="markdownText"
type="textarea"
:autosize="{ minRows: 20 }"
placeholder="正文"
class="markdown-input"
ref="elInputRef"
@click="saveCursor"
@keyup="saveCursor"
/>
</div>
<!-- 右侧Markdown预览框 -->
<div class="preview-pane card">
<div class="editor-title">{{ title || '请输入文章标题' }}</div>
<MarkdownRender :content="markdownText" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { marked } from 'marked'
import hljs from 'highlight.js'
import type { UploadFile } from 'element-plus'
import { markedHighlight } from 'marked-highlight'
import 'highlight.js/styles/github.css'
import { ArrowLeft, Document, Picture, ArrowRight } from '@element-plus/icons-vue'
import MarkdownRender from '@/components/MarkdownRender.vue'
import { useRoute } from 'vue-router'
import DOMPurify from 'dompurify'
const title = ref('')
const markdownText = ref('')
const elInputRef = ref();
const compiledMarkdown = ref('')
const selectedCategory = ref(null)
const categories = [
{ label: '分区1', value: 1 },
{ label: '分区2', value: 2 },
{ label: '分区3', value: 3 },
{ label: '分区4', value: 4 },
{ label: '分区5', value: 5 },
{ label: '分区6', value: 6 },
]
marked.use(
markedHighlight({
langPrefix: 'hljs language-',
highlight: (code: string, lang: string) => {
return hljs.highlightAuto(code, [lang]).value
},
})
)
watch(markdownText, async () => {
compiledMarkdown.value = await marked.parse(markdownText.value)
})
//
const uploadPictureDialog = ref(false)
const handlePicture = () => {
uploadPictureDialog.value = true
}
const previewUrl = ref('');
function handleImageUpload(rawFile: UploadFile) {
const file = rawFile.raw; // File
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result as string;
insertAtCursor(`\n![image](${base64})\n`);
previewUrl.value = base64;
};
reader.readAsDataURL(file);
}
//Markdown
let cursorPos = 0;
//
function saveCursor() {
const textarea = elInputRef.value?.$el.querySelector('textarea');
if (textarea) {
cursorPos = textarea.selectionStart;
}
}
//Markdown
function insertAtCursor(text: string) {
const textarea = elInputRef.value?.$el.querySelector('textarea');
if (!textarea) return;
const current = markdownText.value;
markdownText.value =
current.slice(0, cursorPos) + text + current.slice(cursorPos);
}
</script>
<style scoped lang="scss">
.post-editor {
position:fixed;
top:0;
left:0;
right:0;
height:60px;
padding-right:50px;
padding-left: 16px;
padding-bottom: 16px;
padding-top:16px;
background: #ead1fb;
z-index: 1000;
.toolbar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
.el-button{
height: 55px;
width:55px;
::v-deep(.el-icon)
{
font-size:24px;
}
}
.category-select {
width: 300px;
}
.icon-group {
display: flex;
gap: 6px;
margin-left: 10px;
}
.title-input {
flex-grow: 1;
max-width: 400px;
}
.title-length {
color: #999;
margin-left: 8px;
}
.action-buttons {
margin-left: auto;
display: flex;
gap: 6px;
}
}
}
/* 图片上传窗口样式 */
.upload-dialog {
min-width: 95%;
min-height:300px;
margin:10px;
margin-left:20px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
align-self: center;
.el-dialog__header {
background-color: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.el-dialog__body {
border: 1px dashed #d9d9d9;
border-radius: 6px;
padding: 20px;
}
.avatar-preview {
width: 900px;
height: 500px;
object-fit: cover;
}
}
.editor-body {
display: flex;
flex-direction:row;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-top:20px;
min-height: 100%;
.editor-pane,
.preview-pane {
min-height:93%;
width:50%;
padding: 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.05);
}
.editor-title {
height:50px;
font-weight: bold;
margin-bottom: 16px;
font-size: 25px;
}
.markdown-input {
font-size:20px;
width: 100%;
::v-deep(.el-textarea__inner) {
border:none !important;
box-shadow:none !important;
}
}
}
</style>

@ -1,636 +0,0 @@
<template>
<div class="post-management">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span class="title">我的帖子管理</span>
<el-button type="primary" @click="refreshData">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</template>
<!-- 搜索筛选区域 -->
<div class="filter-section">
<el-form :model="filterForm" inline>
<el-form-item label="帖子标题">
<el-input
v-model="filterForm.title"
placeholder="请输入帖子标题"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="分区">
<el-select
v-model="filterForm.category"
placeholder="请选择分区"
clearable
@change="handleSearch"
>
<el-option label="全部" value="" />
<el-option label="技术讨论" value="tech" />
<el-option label="生活随笔" value="life" />
<el-option label="学习分享" value="study" />
<el-option label="问答求助" value="qa" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch"></el-button>
<el-button @click="handleReset"></el-button>
</el-form-item>
</el-form>
</div>
<!-- 数据统计 -->
<div class="stats-section">
<el-row :gutter="20">
<el-col :span="6">
<el-statistic title="总帖子数" :value="stats.totalPosts" />
</el-col>
<el-col :span="6">
<el-statistic title="总浏览量" :value="stats.totalViews" />
</el-col>
<el-col :span="6">
<el-statistic title="总点赞数" :value="stats.totalLikes" />
</el-col>
<el-col :span="6">
<el-statistic title="平均浏览量" :value="stats.avgViews" :precision="1" />
</el-col>
</el-row>
</div>
<!-- 帖子列表表格 -->
<el-table
v-loading="loading"
:data="posts"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="title" label="帖子标题" min-width="200">
<template #default="{ row }">
<el-link type="primary" @click="viewPost(row.id)">
{{ row.title }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="category" label="分区" width="120">
<template #default="{ row }">
<el-tag :type="getCategoryTagType(row.category)">
{{ getCategoryName(row.category) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="views" label="浏览量" width="100" sortable>
<template #default="{ row }">
<span class="stat-number">{{ row.views }}</span>
</template>
</el-table-column>
<el-table-column prop="likes" label="点赞量" width="100" sortable>
<template #default="{ row }">
<span class="stat-number">{{ row.likes }}</span>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="发布时间" width="180">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'published' ? 'success' : 'warning'">
{{ row.status === 'published' ? '已发布' : '草稿' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="editPost(row.id)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="confirmDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 批量操作 -->
<div v-if="selectedPosts.length > 0" class="batch-actions">
<el-alert
:title="`已选择 ${selectedPosts.length} 个帖子`"
type="info"
show-icon
:closable="false"
/>
<div class="batch-buttons">
<el-button type="danger" @click="confirmBatchDelete">
批量删除
</el-button>
</div>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 删除确认对话框 -->
<el-dialog
v-model="deleteDialog.visible"
title="确认删除"
width="400px"
:before-close="handleCloseDialog"
>
<div class="delete-content">
<el-icon class="warning-icon"><WarningFilled /></el-icon>
<div class="delete-message">
<p v-if="deleteDialog.type === 'single'">
确定要删除帖子 <strong>"{{ deleteDialog.post?.title }}"</strong>
</p>
<p v-else>
确定要删除选中的 <strong>{{ selectedPosts.length }}</strong> 个帖子吗
</p>
<p class="warning-text">此操作不可恢复请谨慎操作</p>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="deleteDialog.visible = false">取消</el-button>
<el-button
type="danger"
:loading="deleteDialog.loading"
@click="handleDelete"
>
确认删除
</el-button>
</span>
</template>
</el-dialog>
<router-link
to="/postEdit"
class="btn btn-primary btn-circle"
>
<span class="btn-icon">+</span>
</router-link>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh, WarningFilled } from '@element-plus/icons-vue'
import axios from 'axios'
//
interface Post {
id: number
title: string
category: string
views: number
likes: number
createdAt: string
status: 'published' | 'draft'
}
interface FilterForm {
title: string
category: string
}
interface Pagination {
currentPage: number
pageSize: number
total: number
}
interface Stats {
totalPosts: number
totalViews: number
totalLikes: number
avgViews: number
}
interface DeleteDialog {
visible: boolean
loading: boolean
type: 'single' | 'batch'
post?: Post
}
//
const loading = ref(false)
const posts = ref<Post[]>([])
const selectedPosts = ref<Post[]>([])
const filterForm = reactive<FilterForm>({
title: '',
category: ''
})
const pagination = reactive<Pagination>({
currentPage: 1,
pageSize: 20,
total: 0
})
const deleteDialog = reactive<DeleteDialog>({
visible: false,
loading: false,
type: 'single'
})
//
const stats = computed<Stats>(() => {
const totalPosts = posts.value.length
const totalViews = posts.value.reduce((sum, post) => sum + post.views, 0)
const totalLikes = posts.value.reduce((sum, post) => sum + post.likes, 0)
const avgViews = totalPosts > 0 ? totalViews / totalPosts : 0
return {
totalPosts,
totalViews,
totalLikes,
avgViews
}
})
// API
const api = axios.create({
baseURL: '/api',
timeout: 10000
})
//
api.interceptors.request.use(
(config) => {
// token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
//
api.interceptors.response.use(
(response) => response,
(error) => {
ElMessage.error(error.response?.data?.message || '请求失败')
return Promise.reject(error)
}
)
//
const fetchPosts = async () => {
loading.value = true
try {
const params = {
page: pagination.currentPage,
pageSize: pagination.pageSize,
title: filterForm.title,
category: filterForm.category
}
const response = await api.get('/posts/my-posts', { params })
posts.value = response.data.data
pagination.total = response.data.total
} catch (error) {
console.error('获取帖子列表失败:', error)
//
posts.value = generateMockData()
pagination.total = 100
} finally {
loading.value = false
}
}
const generateMockData = (): Post[] => {
const categories = ['tech', 'life', 'study', 'qa']
const titles = [
'Vue3 Composition API 最佳实践',
'我的编程学习之路',
'TypeScript 进阶技巧分享',
'如何优化前端性能?',
'生活中的小确幸',
'Element Plus 组件库使用心得'
]
return Array.from({ length: 20 }, (_, index) => ({
id: index + 1,
title: titles[index % titles.length] + ` #${index + 1}`,
category: categories[index % categories.length],
views: Math.floor(Math.random() * 1000) + 100,
likes: Math.floor(Math.random() * 100) + 10,
createdAt: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
status: Math.random() > 0.2 ? 'published' : 'draft'
}))
}
const handleSearch = () => {
pagination.currentPage = 1
fetchPosts()
}
const handleReset = () => {
filterForm.title = ''
filterForm.category = ''
handleSearch()
}
const handleSelectionChange = (selection: Post[]) => {
selectedPosts.value = selection
}
const handleSizeChange = (newSize: number) => {
pagination.pageSize = newSize
fetchPosts()
}
const handleCurrentChange = (newPage: number) => {
pagination.currentPage = newPage
fetchPosts()
}
const refreshData = () => {
fetchPosts()
}
const viewPost = (postId: number) => {
//
window.open(`/posts/${postId}`, '_blank')
}
const editPost = (postId: number) => {
//
window.open(`/posts/${postId}/edit`, '_blank')
}
const confirmDelete = (post: Post) => {
deleteDialog.type = 'single'
deleteDialog.post = post
deleteDialog.visible = true
}
const confirmBatchDelete = () => {
deleteDialog.type = 'batch'
deleteDialog.visible = true
}
const handleDelete = async () => {
deleteDialog.loading = true
try {
if (deleteDialog.type === 'single' && deleteDialog.post) {
await api.delete(`/posts/${deleteDialog.post.id}`)
ElMessage.success('删除成功')
} else if (deleteDialog.type === 'batch') {
const ids = selectedPosts.value.map(post => post.id)
await api.delete('/posts/batch', { data: { ids } })
ElMessage.success(`成功删除 ${ids.length} 个帖子`)
}
deleteDialog.visible = false
selectedPosts.value = []
await fetchPosts()
} catch (error) {
console.error('删除失败:', error)
} finally {
deleteDialog.loading = false
}
}
const handleCloseDialog = () => {
if (!deleteDialog.loading) {
deleteDialog.visible = false
}
}
const getCategoryName = (category: string): string => {
const categoryMap: Record<string, string> = {
tech: '技术讨论',
life: '生活随笔',
study: '学习分享',
qa: '问答求助'
}
return categoryMap[category] || category
}
const getCategoryTagType = (category: string): string => {
const typeMap: Record<string, string> = {
tech: 'primary',
life: 'success',
study: 'warning',
qa: 'info'
}
return typeMap[category] || 'info'
}
const formatDate = (dateString: string): string => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN')
}
//
onMounted(() => {
fetchPosts()
})
</script>
<style scoped lang="scss">
.post-management {
padding: 20px;
font-size: 20px; //
//
:deep(.el-icon) {
font-size: 16px !important;
}
//
:deep(.el-table) {
font-size: 20px;
.el-table__header {
font-size: 20px;
font-weight: 600;
}
}
//
:deep(.el-button) {
font-size: 20px;
}
//
:deep(.el-form-item__label) {
font-size: 20px;
}
:deep(.el-input__inner) {
font-size: 20px;
}
}
.box-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
font-size: 25px; // 18px20px
font-weight: 600;
}
}
}
.filter-section {
margin-bottom: 20px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 8px;
}
.stats-section {
margin-bottom: 20px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
color: white;
:deep(.el-statistic__content) {
color: white;
}
:deep(.el-statistic__head) {
color: rgba(255, 255, 255, 0.8);
font-size: 24px !important; //
}
//
:deep(.el-statistic__number) {
font-size: 36px !important;
}
}
.stat-number {
font-weight: 600;
color: #409EFF;
font-size: 25px; //
}
.batch-actions {
margin: 40px 0;
display: flex;
justify-content: space-between;
align-items: center;
.batch-buttons {
margin-left: 20px;
}
}
.pagination-wrapper {
margin-top: 40px;
display: flex;
justify-content: center;
}
.delete-content {
display: flex;
align-items: flex-start;
.warning-icon {
font-size: 48px; // 24px48px
color: #E6A23C;
margin-right: 12px;
margin-top: 2px;
}
.delete-message {
flex: 1;
p {
margin: 0 0 8px 0;
line-height: 1.5;
font-size: 15px; //
}
.warning-text {
color: #909399;
font-size: 13px; // 12px13px
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
//
@media (max-width: 768px) {
.post-management {
padding: 10px;
}
.filter-section {
:deep(.el-form--inline) {
.el-form-item {
display: block;
margin-right: 0;
margin-bottom: 12px;
}
}
}
.stats-section {
:deep(.el-row) {
.el-col {
margin-bottom: 12px;
}
}
}
:deep(.el-table) {
.el-table-column--selection,
.el-table__column:last-child {
display: none;
}
}
}
</style>

@ -1,395 +0,0 @@
<template>
<div class="search-container">
<!-- 左侧栏 -->
<aside class="sidebar">
<section class="search-standards">
<ul>
<li
v-for="standard in standards"
:key="standard"
:class="{active: selectedStandard === standard}"
@click="selectStandard(standard)"
>{{ standard }}</li>
</ul>
</section>
<hr />
<section class="categories">
<h3>分类</h3>
<div class="category-group">
<div class="category-title">作业/资料</div>
<ul>
<li
v-for="college in colleges"
:key="college"
@click="selectCategory(college)"
:class="{active: selectedCategory === college}"
>
{{ college }}
</li>
<li>(以下省略)</li>
</ul>
</div>
<hr />
<div class="category-group">
<div class="category-title">帖子</div>
<ul>
<li
v-for="postType in postTypes"
:key="postType"
@click="selectCategory(postType)"
:class="{active: selectedCategory === postType}"
>
{{ postType }}
</li>
<li>(以下省略)</li>
</ul>
</div>
</section>
</aside>
<!-- 右侧栏 -->
<main class="search-results">
<div class="search-box">
<input
type="text"
v-model="searchQuery"
placeholder="请输入搜索关键字"
@keypress.enter="doSearch"
aria-label="搜索输入"
/>
<button @click="doSearch">🔍 </button>
</div>
<div class="results-list" v-if="pagedResults.length">
<div v-for="item in pagedResults" :key="item.id" class="result-item">
<div class="content">
<h4>{{ item.title }}</h4>
<p>{{ item.content }}</p>
</div>
<div class="preview-box">
<!-- 这里你可用图片或其它预览示例文本 -->
预览图/内容
</div>
</div>
</div>
<div v-else class="no-results">
暂无搜索结果
</div>
<div class="pagination" v-if="totalPages > 1">
<button @click="prevPage" :disabled="page === 1">上一页</button>
<span> {{ page }} / {{ totalPages }} </span>
<button @click="nextPage" :disabled="page === totalPages">下一页</button>
</div>
</main>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
//
const route = useRoute()
//
const standards = ['综合', '最新', '热度最高', '用户']
const selectedStandard = ref('综合')
//
const colleges = [
'计算机学院',
'遥感学院',
'电子信息学院',
// ...
]
const postTypes = [
'二手集市',
'吐槽树洞',
'校内组局',
// ...
]
const selectedCategory = ref<string | null>(null)
//
const searchQuery = ref('')
const results = ref([
// API
{ id: 1, title: '帖子标题1', content: '帖子内容A' },
{ id: 2, title: '帖子标题2', content: '帖子内容B' },
{ id: 3, title: '帖子标题3', content: '帖子内容C' },
{ id: 4, title: '帖子标题4', content: '帖子内容D' },
{ id: 5, title: '帖子标题5', content: '帖子内容E' },
{ id: 6, title: '帖子标题6', content: '帖子内容F' },
])
//
const page = ref(1)
const pageSize = 3
// queryquery
onMounted(() => {
const queryParam = route.query.query
if (typeof queryParam === 'string' && queryParam.trim()) {
searchQuery.value = queryParam.trim()
doSearch()
}
})
// selectedCategorysearchQuery
const filteredResults = computed(() => {
let filtered = results.value
// 1.
if (selectedCategory.value) {
filtered = filtered.filter(item => item.title.includes(selectedCategory.value!))
}
// 2. /content
if (searchQuery.value) {
const q = searchQuery.value.toLowerCase()
filtered = filtered.filter(item =>
item.title.toLowerCase().includes(q) || item.content.toLowerCase().includes(q)
)
}
// 3. TODO: selectedStandard
//
return filtered
})
const totalPages = computed(() => Math.max(1, Math.ceil(filteredResults.value.length / pageSize)))
const pagedResults = computed(() => {
const start = (page.value - 1) * pageSize
return filteredResults.value.slice(start, start + pageSize)
})
//
function selectStandard(std: string) {
selectedStandard.value = std
page.value = 1
doSearch()
}
function selectCategory(cat: string) {
selectedCategory.value = cat
page.value = 1
doSearch()
}
function doSearch() {
page.value = 1
//
}
function prevPage() {
if (page.value > 1) page.value--
}
function nextPage() {
if (page.value < totalPages.value) page.value++
}
</script>
<style scoped>
.search-container {
width: 1800px;
height: 1000px;
margin: 20px auto;
display: flex;
gap: 24px;
padding: 24px;
background: linear-gradient(90deg, #f6e7f7, #e2e0f9);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 24px;
box-sizing: border-box;
}
/* 左侧栏 */
.sidebar {
flex: 0 0 280px;
background: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 0 12px #ddd;
display: flex;
flex-direction: column;
gap: 16px;
}
.sidebar ul {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar ul li {
margin: 10px 0;
cursor: pointer;
user-select: none;
transition: color 0.2s;
font-size: 1.1em;
}
.sidebar ul li:hover {
color: #7a42f4;
}
.sidebar ul li.active {
font-weight: 700;
color: #581ce0;
}
.categories h3 {
margin-bottom: 12px;
font-size: 1.3em;
font-weight: 700;
}
.category-group {
margin-bottom: 16px;
}
.category-title {
font-weight: 600;
margin-bottom: 8px;
font-size: 1.1em;
}
/* 右侧栏 */
.search-results {
flex: 1;
background: #faf8ff;
padding: 32px;
border-radius: 8px;
box-shadow: 0 0 12px #ccc;
display: flex;
flex-direction: column;
gap: 24px;
overflow: hidden;
}
.search-box {
display: flex;
gap: 12px;
align-items: center;
}
.search-box input {
flex: 1;
padding: 12px 16px;
font-size: 1.2em;
border: 1px solid #ccc;
border-radius: 6px;
outline: none;
transition: border-color 0.2s;
}
.search-box input:focus {
border-color: #765ce6;
}
.search-box button {
cursor: pointer;
padding: 10px 18px;
font-size: 1.1em;
border: none;
background-color: #765ce6;
color: white;
border-radius: 6px;
user-select: none;
transition: background-color 0.2s;
}
.search-box button:hover {
background-color: #5a42b8;
}
.results-list {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
}
.result-item {
display: flex;
justify-content: space-between;
padding: 16px;
background: white;
border: 1px solid #ccc;
border-radius: 8px;
box-sizing: border-box;
}
.content {
flex: 1;
margin-right: 16px;
line-height: 1.5em;
}
.content h4 {
margin: 0 0 8px 0;
font-weight: 600;
font-size: 1.3em;
}
.preview-box {
width: 150px;
background: #ccc;
color: #444;
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px;
user-select: none;
font-size: 1em;
}
.no-results {
font-size: 1.2em;
color: #999;
margin-top: 30px;
text-align: center;
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
}
.pagination button {
cursor: pointer;
border: none;
background: #765ce6;
color: white;
padding: 10px 18px;
border-radius: 6px;
user-select: none;
font-size: 1.1em;
transition: background-color 0.2s;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination button:hover:not(:disabled) {
background-color: #5a42b8;
}
</style>

@ -1,312 +0,0 @@
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
interface Message {
id: number;
content: string;
time: string;
detail: string;
}
export default defineComponent({
setup() {
const messages = ref<Message[]>([
{ id: 1, content: "这是第一条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 10:30" },
{ id: 2, content: "这是第二条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 11:15" },
{ id: 3, content: "这是第三条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 12:00" },
{ id: 4, content: "这是第四条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 12:45" },
{ id: 5, content: "这是第五条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 13:30" },
{ id: 6, content: "这是第六条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 14:15" },
{ id: 7, content: "这是第七条消息内容,点击查看更多", detail:"哈哈哈哈", time: "2025-05-31 15:00" },
//
]);
const currentPage = ref<number>(1);
const messagesPerPage = ref<number>(5);
const isModalOpen = ref<boolean>(false);
const selectedMessage = ref<Message | null>(null);
const totalPages = computed((): number => {
return Math.ceil(messages.value.length / messagesPerPage.value);
});
const currentPageMessages = computed((): Message[] => {
const startIndex = (currentPage.value - 1) * messagesPerPage.value;
const endIndex = startIndex + messagesPerPage.value;
return messages.value.slice(startIndex, endIndex);
});
const changePage = (pageNumber: number): void => {
if (pageNumber >= 1 && pageNumber <= totalPages.value) {
currentPage.value = pageNumber;
}
};
const openModal = (message : Message) :void => {
selectedMessage.value = message;
isModalOpen.value = true;
document.body.style.overflow="hidden";
}
const closeModal = () :void => {
selectedMessage.value = null;
isModalOpen.value = false;
document.body.style.overflow='';
}
return {
messages,
currentPage,
messagesPerPage,
totalPages,
currentPageMessages,
changePage,
isModalOpen,
selectedMessage,
openModal,
closeModal,
};
}
});
</script>
<template>
<div class="message-center">
<!-- 右侧主要内容区域 -->
<div class="main-content">
<!-- 顶部标题区域 -->
<div class="content-header">
<span class="header-title">评论回复</span>
</div>
<!-- 消息框区域 - 现在是垂直排列 -->
<div class="message-container">
<div v-for="(message, index) in currentPageMessages"
:key="index"
class="message-box"
@click="openModal(message)">
<div class="message-content">{{ message.content }}</div>
<div class="message-time">{{ message.time }}</div>
</div>
</div>
<!-- 分页控制区域 -->
<div class="pagination-controls" v-if="totalPages > 1">
<button
class="page-button"
:disabled="currentPage === 1"
@click="changePage(currentPage - 1)"
>
上一页
</button>
<span class="page-indicator">{{ currentPage }} / {{ totalPages }}</span>
<button
class="page-button"
:disabled="currentPage === totalPages"
@click="changePage(currentPage + 1)"
>
下一页
</button>
</div>
</div>
<div v-if="isModalOpen" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h3>消息详情</h3>
</div>
<div class="modal-body">
<p>{{ selectedMessage?.detail }}</p>
</div>
<div class="modal-footer">
<div class="message-time">{{ selectedMessage?.time }}</div>
<button class="confirm-button" @click="closeModal"></button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.message-center {
display: flex;
width: calc(100% - 100px); /* 减去侧边栏宽度 */
height: calc(100vh - 120px);
margin: 130px 20px 20px 350px; /* 左侧margin增加到120px为侧边栏留出空间 */
padding: 0;
border-radius: 10px;
}
/* 主要内容区域样式 */
.main-content {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
}
/* 顶部标题区域样式 */
.content-header {
height: 70px;
width:1267px;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
border: 2px solid rgba(133, 88, 207, 0.5);
border-radius: 10px;
margin-bottom: 10px;
display: flex;
align-items: center;
padding: 0 20px;
}
.header-title {
font-size: 30px;
font-weight: bold;
color: #7e7e7e;
}
/* 消息框容器样式 - 修改为垂直排列 */
.message-container {
display: flex;
flex-direction: column; /* 改为垂直排列 */
gap: 15px; /* 消息框之间的间距 */
margin-bottom: 20px;
overflow-y: auto; /* 如果内容过多可以滚动 */
}
/* 单个消息框样式 */
.message-box {
width: 1275px; /* 宽度占满容器 */
min-height: 80px; /* 减小高度使垂直排列更紧凑 */
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(5px);
border: 1px solid rgba(133, 88, 207, 0.3);
border-radius: 8px;
padding: 15px;
display: flex;
flex-direction: column;
justify-content: space-between;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.message-box:hover{
transform: translateY(-3px);
box-shadow: 0 20px 10px rgba(0, 0, 0, 0.1);
}
.message-content{
font-size: 24px;
color:#333;
margin-bottom: 10px;
flex-grow: 1;
}
.message-time{
font-size: 18px;
color:#888;
text-align: right;
}
.pagination-controls{
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.page-button{
background: rgba(133, 88, 207, 0.7);
color: white;
border: none;
border-radius: 5px;
padding: 8px 15px;
cursor: pointer;
margin: 0 10px;
transition:background 0.2s;
position: relative;
right: 200px;
}
.page-button:hover:not(:disabled){
background: rgba(133, 88, 207, 0.9);
}
.page-button:disabled{
background: rgba(133, 88, 207, 0.3);
cursor: not-allowed;
}
.page-indicator{
font-size: 14px;
color: #666;
position: relative;
right: 200px;
}
.modal-overlay{
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content{
background: #fff;
width: 600px;
border-radius: 10px;
box-shadow: 0 5px 30px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
height: 800px;
font-size: 28px;
}
.modal-header{
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3{
margin: 0;
color: #333;
}
.modal-body{
padding: 20px;
overflow-y: visible;
flex-grow: 1;
line-height: 1.6;
}
.modal-footer{
padding: 15px 20px;
border-top: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.confirm-button{
background: rgba(133, 88, 207, 0.7);
color: white;
border: none;
border-radius: 5px;
padding: 8px 20px;
cursor: pointer;
transition: background 0.2s;
}
.confirm-button:hover{
background: rgba(133, 88, 207, 0.9);
}
</style>

@ -1,111 +0,0 @@
// types/post.ts
export interface Post {
id: number
title: string
content?: string
category: string
views: number
likes: number
comments?: number
createdAt: string
updatedAt?: string
status: 'published' | 'draft'
author?: {
id: number
username: string
avatar?: string
}
}
export interface PostListParams {
page: number
pageSize: number
title?: string
category?: string
status?: string
sortBy?: 'createdAt' | 'views' | 'likes'
sortOrder?: 'asc' | 'desc'
}
export interface PostListResponse {
data: Post[]
total: number
page: number
pageSize: number
totalPages: number
}
export interface FilterForm {
title: string
category: string
status?: string
}
export interface Pagination {
currentPage: number
pageSize: number
total: number
}
export interface Stats {
totalPosts: number
totalViews: number
totalLikes: number
totalComments: number
avgViews: number
avgLikes: number
}
export interface DeleteDialog {
visible: boolean
loading: boolean
type: 'single' | 'batch'
post?: Post
}
// types/api.ts
export interface ApiResponse<T = any> {
code: number
message: string
data: T
timestamp: number
}
export interface ApiError {
code: number
message: string
details?: any
}
// types/category.ts
export interface Category {
id: string
name: string
description?: string
color?: string
icon?: string
}
export const CATEGORIES: Category[] = [
{ id: 'tech', name: '技术讨论', color: 'primary', icon: 'Monitor' },
{ id: 'life', name: '生活随笔', color: 'success', icon: 'Coffee' },
{ id: 'study', name: '学习分享', color: 'warning', icon: 'Reading' },
{ id: 'qa', name: '问答求助', color: 'info', icon: 'QuestionFilled' }
]
// types/user.ts
export interface User {
id: number
username: string
email: string
avatar?: string
role: 'admin' | 'user'
createdAt: string
profile?: {
nickname?: string
bio?: string
location?: string
website?: string
}
}

@ -1,138 +0,0 @@
// hooks/usePost.ts
import { ref, reactive, computed } from 'vue'
import type { Post, PostListParams, FilterForm, Pagination, Stats } from '@/views/post'
import { postApi } from '@/utils/post'
import { ElMessage } from 'element-plus'
export function usePostManagement() {
const loading = ref(false)
const posts = ref<Post[]>([])
const selectedPosts = ref<Post[]>([])
const filterForm = reactive<FilterForm>({
title: '',
category: '',
status: ''
})
const pagination = reactive<Pagination>({
currentPage: 1,
pageSize: 20,
total: 0
})
// 计算统计数据
const stats = computed<Stats>(() => {
const totalPosts = posts.value.length
const totalViews = posts.value.reduce((sum, post) => sum + post.views, 0)
const totalLikes = posts.value.reduce((sum, post) => sum + post.likes, 0)
const totalComments = posts.value.reduce((sum, post) => sum + (post.comments || 0), 0)
const avgViews = totalPosts > 0 ? totalViews / totalPosts : 0
const avgLikes = totalPosts > 0 ? totalLikes / totalPosts : 0
return {
totalPosts,
totalViews,
totalLikes,
totalComments,
avgViews,
avgLikes
}
})
// 获取帖子列表
const fetchPosts = async () => {
loading.value = true
try {
const params: PostListParams = {
page: pagination.currentPage,
pageSize: pagination.pageSize,
title: filterForm.title || undefined,
category: filterForm.category || undefined,
status: filterForm.status || undefined
}
const response = await postApi.getMyPosts(params)
posts.value = response.data
pagination.total = response.total
} catch (error) {
ElMessage.error('获取帖子列表失败')
console.error('Fetch posts error:', error)
} finally {
loading.value = false
}
}
// 删除帖子
const deletePost = async (postId: number): Promise<boolean> => {
try {
await postApi.deletePost(postId)
ElMessage.success('删除成功')
return true
} catch (error) {
ElMessage.error('删除失败')
console.error('Delete post error:', error)
return false
}
}
// 批量删除
const batchDeletePosts = async (postIds: number[]): Promise<boolean> => {
try {
await postApi.batchDeletePosts(postIds)
ElMessage.success(`成功删除 ${postIds.length} 个帖子`)
return true
} catch (error) {
ElMessage.error('批量删除失败')
console.error('Batch delete posts error:', error)
return false
}
}
// 搜索
const handleSearch = () => {
pagination.currentPage = 1
fetchPosts()
}
// 重置筛选
const handleReset = () => {
Object.keys(filterForm).forEach(key => {
filterForm[key as keyof FilterForm] = ''
})
handleSearch()
}
// 选择变化
const handleSelectionChange = (selection: Post[]) => {
selectedPosts.value = selection
}
// 分页变化
const handleSizeChange = (newSize: number) => {
pagination.pageSize = newSize
fetchPosts()
}
const handleCurrentChange = (newPage: number) => {
pagination.currentPage = newPage
fetchPosts()
}
return {
loading,
posts,
selectedPosts,
filterForm,
pagination,
stats,
fetchPosts,
deletePost,
batchDeletePosts,
handleSearch,
handleReset,
handleSelectionChange,
handleSizeChange,
handleCurrentChange
}
}

@ -1,26 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"exclude": [
"src/**/*_test.vue",
"src/**/*.test.vue",
"src/**/*.spec.vue",
"src/**/__tests__/*",
"src/components/Personal/AcountManager_test.vue"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 419 KiB

@ -1,87 +0,0 @@
# 项目配置说明
## 环境配置
本项目使用环境变量和本地配置文件来管理敏感信息,确保安全性。
### 1. 本地开发环境配置
创建 `src/main/resources/application-local.yml` 文件(已在.gitignore中忽略
```yaml
# 本地开发环境配置
aliyun:
oss:
endpoint: your-endpoint
accessKeyId: your-access-key-id
accessKeySecret: your-access-key-secret
bucketName: your-bucket-name
urlPrefix: https://your-bucket-name.oss-region.aliyuncs.com/
```
### 2. 环境变量配置
复制 `env.example``.env` 并填入真实配置:
```bash
cp env.example .env
```
然后编辑 `.env` 文件,填入您的真实配置信息。
### 3. 启动应用
#### 方式1使用本地配置文件
```bash
java -jar app.jar --spring.profiles.active=local
```
#### 方式2使用环境变量
```bash
# 设置环境变量
export ALIYUN_OSS_ENDPOINT=your-endpoint
export ALIYUN_OSS_ACCESS_KEY_ID=your-access-key-id
export ALIYUN_OSS_ACCESS_KEY_SECRET=your-access-key-secret
export ALIYUN_OSS_BUCKET_NAME=your-bucket-name
export ALIYUN_OSS_URL_PREFIX=https://your-bucket-name.oss-region.aliyuncs.com/
# 启动应用
java -jar app.jar
```
## 阿里云OSS配置
### 1. 创建OSS Bucket
1. 登录阿里云控制台
2. 进入对象存储OSS服务
3. 创建Bucket选择合适的地域和存储类型
4. 配置访问权限(推荐私有读写)
### 2. 获取AccessKey
1. 进入阿里云控制台
2. 点击右上角头像 -> AccessKey管理
3. 创建AccessKey建议使用RAM子账号
4. 为RAM用户授予OSS相关权限
### 3. 配置跨域访问CORS
在OSS控制台设置CORS规则
- 来源:您的前端域名
- 允许MethodsGET, POST, PUT, DELETE, HEAD
- 允许Headers*
- 暴露HeadersETag, x-oss-request-id
## 安全注意事项
1. **永远不要将AccessKey提交到代码仓库**
2. **使用RAM子账号最小权限原则**
3. **定期轮换AccessKey**
4. **启用OSS访问日志监控**
5. **配置适当的Bucket策略**
## 生产环境部署
生产环境建议使用以下方式之一:
1. **容器环境变量**Docker/Kubernetes
2. **云服务商的密钥管理服务**
3. **专门的配置中心**如Nacos、Apollo

@ -1,72 +0,0 @@
# 简化的AI聊天会话历史方案
## 现状分析
您的项目已经有了完整的Spring AI ChatMemory + MySQL实现
- ✅ Spring AI自动将消息存储到 `SPRING_AI_CHAT_MEMORY`
- ✅ 自动会话记忆功能20条消息窗口
- ✅ 消息持久化到MySQL
## 缺失的功能
Spring AI ChatMemory 专注于消息存储,但缺少:
- ❌ 会话列表管理
- ❌ 会话标题管理
- ❌ 会话创建时间等元数据
## 建议的简化方案
### 1. 保留的表结构
只需要一个会话管理表:
```sql
-- 会话元数据管理表补充Spring AI ChatMemory
CREATE TABLE IF NOT EXISTS ai_chat_sessions (
id VARCHAR(64) PRIMARY KEY COMMENT '会话ID',
user_id BIGINT NULL COMMENT '用户ID可选',
title VARCHAR(200) NOT NULL DEFAULT '新对话' COMMENT '会话标题',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_user_id (user_id),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
### 2. 删除冗余的表
可以删除:
- `ai_chat_messages_history` 表(与 SPRING_AI_CHAT_MEMORY 重复)
### 3. 简化的服务层
只需要会话元数据管理,消息历史直接从 `SPRING_AI_CHAT_MEMORY` 查询:
```java
// 获取会话消息历史 - 直接查询Spring AI表
@Override
public Result<AiMessageHistoryVO> getSessionMessages(String sessionId, Integer page, Integer size) {
// 直接从Spring AI ChatMemory获取
List<Message> messages = chatMemory.get(sessionId);
// 转换为VO并返回
// ... 转换逻辑
}
```
### 4. 保留的核心功能
- 会话列表管理
- 会话标题管理
- 会话创建/删除
- 从 SPRING_AI_CHAT_MEMORY 表直接查询消息历史
## 实际需要的修改
1. **删除冗余表**: 移除 `ai_chat_messages_history`
2. **简化服务**: 移除消息同步逻辑直接使用Spring AI ChatMemory
3. **保留会话管理**: 只管理会话元数据
## 结论
您的担心是对的大部分功能确实是冗余的。Spring AI ChatMemory已经提供了强大的消息存储和记忆功能我们只需要补充会话元数据管理即可。

@ -1,28 +0,0 @@
# 环境变量配置示例
# 复制此文件为 .env 并填入真实配置
# 阿里云OSS配置
ALIYUN_OSS_ENDPOINT=your-endpoint
ALIYUN_OSS_ACCESS_KEY_ID=your-access-key-id
ALIYUN_OSS_ACCESS_KEY_SECRET=your-access-key-secret
ALIYUN_OSS_BUCKET_NAME=your-bucket-name
ALIYUN_OSS_URL_PREFIX=https://your-bucket-name.oss-region.aliyuncs.com/
# 数据库配置
DB_URL=jdbc:mysql://localhost:3306/UniLife?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
DB_USERNAME=root
DB_PASSWORD=123456
# Redis配置
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
# JWT配置
JWT_SECRET=qwertyuiopasdfghjklzxcvbnm
JWT_EXPIRATION=86400
# 邮箱配置
MAIL_HOST=smtp.163.com
MAIL_PORT=465
MAIL_USERNAME=your-email@163.com
MAIL_PASSWORD=your-auth-code

@ -1,256 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>unilife-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backend</name>
<description>backend</description>
<properties>
<java.version>17</java.version>
<spring-boot.version>3.4.3</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>
<!-- IP2Region -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.7.0</version>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<!-- Knife4j - OpenAPI3 with Jakarta -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- MockMvc for web layer testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<scope>test</scope>
</dependency>
<!-- Mockito for mocking -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<!-- H2 Database for testing -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- TestContainers for integration testing -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 阿里云OSS -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.0</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<!-- Spring AI JDBC Chat Memory Repository for MySQL -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<!-- Spring AI Chroma Vector Store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-chroma</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 编译器插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<parameters>true</parameters>
</configuration>
</plugin>
<!-- Spring Boot 插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<!-- 确保Lombok注解处理器不会干扰参数名保留 -->
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -1,37 +0,0 @@
package com.unilife.common.constant;
public class Prompt {
public static final String Prompt = """
#
AIUniLife
#
1. ****
*
2. ****
* []
* ****
3. ****
*
*
*
4. ****
* ****
*
*
*
*
* ****
*
5. ****
*
*
* 使
""";
}

@ -1,100 +0,0 @@
package com.unilife.config;
import com.unilife.common.constant.Prompt;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AiConfig {
/**
*
*/
@Value("${app.ai.auto-title.enabled:true}")
private boolean autoTitleEnabled;
/**
* simple() ai(AI)
*/
@Value("${app.ai.auto-title.strategy:simple}")
private String titleGenerationStrategy;
/**
* ChatMemory使JDBC
*/
@Bean
public ChatMemory chatMemory(ChatMemoryRepository chatMemoryRepository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(chatMemoryRepository)
.maxMessages(20) // 保留最近20条消息作为上下文
.build();
}
/**
* PDF
*/
@Bean("pdfVectorTaskExecutor")
public Executor pdfVectorTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2); // 核心线程数
executor.setMaxPoolSize(5); // 最大线程数
executor.setQueueCapacity(100); // 队列容量
executor.setThreadNamePrefix("pdf-vector-");
executor.setKeepAliveSeconds(60); // 线程空闲时间
executor.initialize();
return executor;
}
/**
* ChatClientChat Memory
*/
@Bean
public ChatClient chatClient(OpenAiChatModel model, ChatMemory chatMemory, VectorStore vectorStore) {
return ChatClient.builder(model)
.defaultSystem(Prompt.Prompt) // 设置默认系统提示
.defaultAdvisors(
new SimpleLoggerAdvisor(),
MessageChatMemoryAdvisor.builder(chatMemory).build(),
QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.similarityThreshold(0.5d)
.topK(5)
.build())
.build()
)
.build();
}
/**
*
*/
public boolean isAutoTitleEnabled() {
return autoTitleEnabled;
}
/**
*
*/
public String getTitleGenerationStrategy() {
return titleGenerationStrategy;
}
}

@ -1,39 +0,0 @@
package com.unilife.config;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OssConfig {
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
@Value("${aliyun.oss.urlPrefix}")
private String urlPrefix;
@Bean
public OSS ossClient() {
return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}
public String getBucketName() {
return bucketName;
}
public String getUrlPrefix() {
return urlPrefix;
}
}

@ -1,18 +0,0 @@
package com.unilife.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info().title("UniLife API").version("1.0.0"));
}
}

@ -1,77 +0,0 @@
package com.unilife.config;
import com.unilife.interceptor.AdminInterceptor;
import com.unilife.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.io.File;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Autowired
private AdminInterceptor adminInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 管理员权限拦截器 - 优先级最高
registry.addInterceptor(adminInterceptor)
.addPathPatterns("/admin/**")
.order(1);
// JWT拦截器
registry.addInterceptor(jwtInterceptor).addPathPatterns("/**")
.excludePathPatterns(
// 用户登录注册相关
"/users/login",
"/users/register",
"/users/code",
"/users/login/code",
// 静态资源访问
"/api/files/**",
// 管理员接口由AdminInterceptor处理
"/admin/**",
// Swagger文档相关
"/swagger-resources/**",
"/v3/api-docs/**",
"/doc.html",
"/webjars/**",
"/favicon.ico",
"/knife4j/**"
)
.order(2);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 配置上传文件的访问路径
String uploadPath = new File("uploads").getAbsolutePath();
registry.addResourceHandler("/api/files/**")
.addResourceLocations("file:" + uploadPath + File.separator);
// 直接映射到uploads/resources目录
registry.addResourceHandler("/api/resources/**")
.addResourceLocations("file:" + new File("uploads/resources").getAbsolutePath() + File.separator);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*") // 允许所有来源,生产环境建议限制为特定域名
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

@ -1,146 +0,0 @@
package com.unilife.controller;
import com.unilife.common.result.Result;
import com.unilife.service.AdminService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/admin")
@Tag(name = "管理员接口", description = "后台管理相关接口")
public class AdminController {
@Autowired
private AdminService adminService;
@Operation(summary = "获取系统统计数据")
@GetMapping("/stats")
public Result getSystemStats() {
return adminService.getSystemStats();
}
@Operation(summary = "获取用户列表")
@GetMapping("/users")
public Result getUserList(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Integer role,
@RequestParam(required = false) Integer status) {
return adminService.getUserList(page, size, keyword, role, status);
}
@Operation(summary = "更新用户状态")
@PutMapping("/users/{userId}/status")
public Result updateUserStatus(@PathVariable Long userId, @RequestBody Map<String, Integer> request) {
Integer status = request.get("status");
return adminService.updateUserStatus(userId, status);
}
@Operation(summary = "更新用户角色")
@PutMapping("/users/{userId}/role")
public Result updateUserRole(@PathVariable Long userId, @RequestBody Map<String, Integer> request) {
Integer role = request.get("role");
return adminService.updateUserRole(userId, role);
}
@Operation(summary = "删除用户")
@DeleteMapping("/users/{userId}")
public Result deleteUser(@PathVariable Long userId) {
return adminService.deleteUser(userId);
}
@Operation(summary = "获取帖子列表")
@GetMapping("/posts")
public Result getPostList(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) Integer status) {
return adminService.getPostList(page, size, keyword, categoryId, status);
}
@Operation(summary = "更新帖子状态")
@PutMapping("/posts/{postId}/status")
public Result updatePostStatus(@PathVariable Long postId, @RequestBody Map<String, Integer> request) {
Integer status = request.get("status");
return adminService.updatePostStatus(postId, status);
}
@Operation(summary = "删除帖子")
@DeleteMapping("/posts/{postId}")
public Result deletePost(@PathVariable Long postId) {
return adminService.deletePost(postId);
}
@Operation(summary = "永久删除帖子")
@DeleteMapping("/posts/{postId}/permanent")
public Result permanentDeletePost(@PathVariable Long postId) {
return adminService.permanentDeletePost(postId);
}
@Operation(summary = "获取评论列表")
@GetMapping("/comments")
public Result getCommentList(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long postId,
@RequestParam(required = false) Integer status) {
return adminService.getCommentList(page, size, keyword, postId, status);
}
@Operation(summary = "删除评论")
@DeleteMapping("/comments/{commentId}")
public Result deleteComment(@PathVariable Long commentId) {
return adminService.deleteComment(commentId);
}
@Operation(summary = "获取分类列表")
@GetMapping("/categories")
public Result getCategoryList() {
return adminService.getCategoryList();
}
@Operation(summary = "创建分类")
@PostMapping("/categories")
public Result createCategory(@RequestBody Map<String, Object> request) {
return adminService.createCategory(request);
}
@Operation(summary = "更新分类")
@PutMapping("/categories/{categoryId}")
public Result updateCategory(@PathVariable Long categoryId, @RequestBody Map<String, Object> request) {
return adminService.updateCategory(categoryId, request);
}
@Operation(summary = "删除分类")
@DeleteMapping("/categories/{categoryId}")
public Result deleteCategory(@PathVariable Long categoryId) {
return adminService.deleteCategory(categoryId);
}
@Operation(summary = "获取资源列表")
@GetMapping("/resources")
public Result getResourceList(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long categoryId,
@RequestParam(required = false) Integer status) {
return adminService.getResourceList(page, size, keyword, categoryId, status);
}
@Operation(summary = "删除资源")
@DeleteMapping("/resources/{resourceId}")
public Result deleteResource(@PathVariable Long resourceId) {
return adminService.deleteResource(resourceId);
}
}

@ -1,94 +0,0 @@
package com.unilife.controller;
import com.unilife.common.result.Result;
import com.unilife.model.dto.AiCreateSessionDTO;
import com.unilife.model.dto.AiSendMessageDTO;
import com.unilife.model.dto.AiUpdateSessionDTO;
import com.unilife.model.vo.AiCreateSessionVO;
import com.unilife.model.vo.AiMessageHistoryVO;
import com.unilife.model.vo.AiSessionListVO;
import com.unilife.service.AiService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@RestController
@Slf4j
@RequestMapping("/ai")
@Tag(name = "AI辅助学习")
@RequiredArgsConstructor
public class AiController {
private final AiService aiService;
@Operation(summary = "发送消息给AI")
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> sendMessage(
@RequestParam("prompt") String prompt,
@RequestParam(value = "sessionId", required = false) String sessionId) {
log.info("发送消息给AI: {}", prompt);
AiSendMessageDTO sendMessageDTO = new AiSendMessageDTO();
sendMessageDTO.setMessage(prompt);
sendMessageDTO.setSessionId(sessionId);
return aiService.sendMessage(sendMessageDTO);
}
@Operation(summary = "获取聊天会话列表")
@GetMapping("/sessions")
public Result<AiSessionListVO> getSessionList(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") Integer size) {
log.info("获取会话列表,页码: {}, 每页大小: {}", page, size);
return aiService.getSessionList(page, size);
}
@Operation(summary = "获取会话消息历史")
@GetMapping("/sessions/{sessionId}/messages")
public Result<AiMessageHistoryVO> getSessionMessages(
@Parameter(description = "会话ID") @PathVariable String sessionId,
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "50") Integer size) {
log.info("获取会话消息历史会话ID: {}, 页码: {}, 每页大小: {}", sessionId, page, size);
return aiService.getSessionMessages(sessionId, page, size);
}
@Operation(summary = "创建聊天会话")
@PostMapping("/sessions")
public Result<AiCreateSessionVO> createSession(@RequestBody AiCreateSessionDTO createSessionDTO) {
log.info("创建聊天会话: {}", createSessionDTO.getSessionId());
return aiService.createSession(createSessionDTO);
}
@Operation(summary = "更新会话标题")
@PutMapping("/sessions/{sessionId}")
public Result<Void> updateSessionTitle(
@Parameter(description = "会话ID") @PathVariable String sessionId,
@RequestBody AiUpdateSessionDTO updateSessionDTO) {
log.info("更新会话标题会话ID: {}, 新标题: {}", sessionId, updateSessionDTO.getTitle());
return aiService.updateSessionTitle(sessionId, updateSessionDTO);
}
@Operation(summary = "清空会话消息")
@DeleteMapping("/sessions/{sessionId}/messages")
public Result<Void> clearSessionMessages(@Parameter(description = "会话ID") @PathVariable String sessionId) {
log.info("清空会话消息会话ID: {}", sessionId);
return aiService.clearSessionMessages(sessionId);
}
@Operation(summary = "删除会话")
@DeleteMapping("/sessions/{sessionId}")
public Result<Void> deleteSession(@Parameter(description = "会话ID") @PathVariable String sessionId) {
log.info("删除会话会话ID: {}", sessionId);
return aiService.deleteSession(sessionId);
}
}

@ -1,84 +0,0 @@
package com.unilife.controller;
import com.unilife.common.result.Result;
import com.unilife.model.entity.Category;
import com.unilife.service.CategoryService;
import com.unilife.utils.BaseContext;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Tag(name = "分类管理")
@RestController
@RequestMapping("/categories")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;
@Operation(summary = "获取分类详情")
@GetMapping("/{id}")
public Result<?> getCategoryDetail(@PathVariable("id") Long categoryId) {
return categoryService.getCategoryDetail(categoryId);
}
@Operation(summary = "获取分类列表")
@GetMapping
public Result<?> getCategoryList(
@RequestParam(value = "status", required = false) Byte status) {
return categoryService.getCategoryList(status);
}
@Operation(summary = "创建分类")
@PostMapping
public Result<?> createCategory(@RequestBody Category category) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
// 检查用户权限(只有管理员可以创建分类)
// 实际项目中应该从用户服务获取用户角色信息
// 这里简化处理,假设已经检查了权限
return categoryService.createCategory(category);
}
@Operation(summary = "更新分类")
@PutMapping("/{id}")
public Result<?> updateCategory(
@PathVariable("id") Long categoryId,
@RequestBody Category category) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
// 检查用户权限(只有管理员可以更新分类)
// 实际项目中应该从用户服务获取用户角色信息
// 这里简化处理,假设已经检查了权限
return categoryService.updateCategory(categoryId, category);
}
@Operation(summary = "删除分类")
@DeleteMapping("/{id}")
public Result<?> deleteCategory(@PathVariable("id") Long categoryId) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
// 检查用户权限(只有管理员可以删除分类)
// 实际项目中应该从用户服务获取用户角色信息
// 这里简化处理,假设已经检查了权限
return categoryService.deleteCategory(categoryId);
}
}

@ -1,62 +0,0 @@
package com.unilife.controller;
import com.unilife.common.result.Result;
import com.unilife.model.dto.CreateCommentDTO;
import com.unilife.service.CommentService;
import com.unilife.utils.BaseContext;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Tag(name = "评论管理")
@RestController
@RequestMapping("/comments")
@Slf4j
public class CommentController {
@Autowired
private CommentService commentService;
@Operation(summary = "创建评论")
@PostMapping
public Result<?> createComment(@RequestBody CreateCommentDTO createCommentDTO) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return commentService.createComment(userId, createCommentDTO);
}
@Operation(summary = "获取帖子评论列表")
@GetMapping("/post/{postId}")
public Result<?> getCommentsByPostId(@PathVariable("postId") Long postId) {
// 从当前上下文获取用户ID可能为null未登录用户
Long userId = BaseContext.getId();
return commentService.getCommentsByPostId(postId, userId);
}
@Operation(summary = "删除评论")
@DeleteMapping("/{id}")
public Result<?> deleteComment(@PathVariable("id") Long commentId) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return commentService.deleteComment(commentId, userId);
}
@Operation(summary = "点赞/取消点赞评论")
@PostMapping("/{id}/like")
public Result<?> likeComment(@PathVariable("id") Long commentId) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return commentService.likeComment(commentId, userId);
}
}

@ -1,115 +0,0 @@
package com.unilife.controller;
import com.unilife.common.result.Result;
import com.unilife.model.dto.CreateCourseDTO;
import com.unilife.service.CourseService;
import com.unilife.utils.BaseContext;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Tag(name = "课程管理")
@RestController
@RequestMapping("/courses")
@Slf4j
public class CourseController {
@Autowired
private CourseService courseService;
@Operation(summary = "创建课程")
@PostMapping
public Result<?> createCourse(@RequestBody CreateCourseDTO createCourseDTO) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return courseService.createCourse(userId, createCourseDTO);
}
@Operation(summary = "获取课程详情")
@GetMapping("/{id}")
public Result<?> getCourseDetail(@PathVariable("id") Long courseId) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return courseService.getCourseDetail(courseId, userId);
}
@Operation(summary = "获取用户的所有课程")
@GetMapping
public Result<?> getCourseList() {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return courseService.getCourseList(userId);
}
@Operation(summary = "获取用户在指定星期几的课程")
@GetMapping("/day/{dayOfWeek}")
public Result<?> getCourseListByDayOfWeek(@PathVariable("dayOfWeek") Byte dayOfWeek) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return courseService.getCourseListByDayOfWeek(userId, dayOfWeek);
}
@Operation(summary = "获取用户在指定学期的课程")
@GetMapping("/semester/{semester}")
public Result<?> getCourseListBySemester(@PathVariable("semester") String semester) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return courseService.getCourseListBySemester(userId, semester);
}
@Operation(summary = "更新课程")
@PutMapping("/{id}")
public Result<?> updateCourse(
@PathVariable("id") Long courseId,
@RequestBody CreateCourseDTO createCourseDTO) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return courseService.updateCourse(courseId, userId, createCourseDTO);
}
@Operation(summary = "删除课程")
@DeleteMapping("/{id}")
public Result<?> deleteCourse(@PathVariable("id") Long courseId) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return courseService.deleteCourse(courseId, userId);
}
@Operation(summary = "检查课程时间冲突")
@GetMapping("/check-conflict")
public Result<?> checkCourseConflict(
@RequestParam("dayOfWeek") Byte dayOfWeek,
@RequestParam("startTime") String startTime,
@RequestParam("endTime") String endTime,
@RequestParam(value = "excludeCourseId", required = false) Long excludeCourseId) {
// 从当前上下文获取用户ID
Long userId = BaseContext.getId();
if (userId == null) {
return Result.error(401, "未登录");
}
return courseService.checkCourseConflict(userId, dayOfWeek, startTime, endTime, excludeCourseId);
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save