@ -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&useSSL=false&serverTimezone=UTC&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>
|
Before Width: | Height: | Size: 184 KiB |
Before Width: | Height: | Size: 10 KiB |
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>
|
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 831 B |
Before Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 335 B |
Before Width: | Height: | Size: 184 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 231 KiB |
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,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,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,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,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,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,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"
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 207 KiB |
Before Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 181 KiB |
Before Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 125 KiB |
Before Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 222 KiB |
Before Width: | Height: | Size: 146 KiB |
Before Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 179 KiB |
Before Width: | Height: | Size: 111 KiB |
Before Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 166 KiB |
Before Width: | Height: | Size: 419 KiB |
@ -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,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,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,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,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);
|
||||
}
|
||||
}
|