Compare commits
97 Commits
@ -0,0 +1,5 @@
|
|||||||
|
UniLife开发进度与计划.md
|
||||||
|
UniLife接口文档.md
|
||||||
|
UniLife项目文档.md
|
||||||
|
文档说明.md
|
||||||
|
.idea/
|
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ApifoxUploaderProjectSetting">
|
||||||
|
<option name="apiAccessToken" value="APS-Du6Eoh7CmJJcLtPBY40931ru8MEovCoV" />
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -0,0 +1,10 @@
|
|||||||
|
<?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>
|
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<annotationProcessing>
|
||||||
|
<profile default="true" name="Default" enabled="true" />
|
||||||
|
<profile name="Maven default annotation processors profile" enabled="true">
|
||||||
|
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||||
|
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||||
|
<outputRelativeToContentRoot value="true" />
|
||||||
|
<module name="unilife-server" />
|
||||||
|
</profile>
|
||||||
|
</annotationProcessing>
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -0,0 +1,48 @@
|
|||||||
|
<?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>
|
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="file://$PROJECT_DIR$/unilife-server/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/unilife-server/src/main/resources" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RemoteRepositoriesConfiguration">
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Central Repository" />
|
||||||
|
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Maven Central repository" />
|
||||||
|
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="jboss.community" />
|
||||||
|
<option name="name" value="JBoss Community repository" />
|
||||||
|
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Central Repository" />
|
||||||
|
<option name="url" value="https://maven.aliyun.com/repository/public" />
|
||||||
|
</remote-repository>
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="MavenProjectsManager">
|
||||||
|
<option name="originalFiles">
|
||||||
|
<list>
|
||||||
|
<option value="$PROJECT_DIR$/backend/pom.xml" />
|
||||||
|
<option value="$PROJECT_DIR$/unilife-server/pom.xml" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/unilife.iml" filepath="$PROJECT_DIR$/.idea/unilife.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -0,0 +1,6 @@
|
|||||||
|
<?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>
|
@ -0,0 +1,124 @@
|
|||||||
|
<?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>
|
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
After Width: | Height: | Size: 184 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 9.9 KiB |
@ -0,0 +1,307 @@
|
|||||||
|
/* 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,11 +1,28 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import LogPage from './components/LogPage.vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LogPage/>
|
<HeaderBar v-if = "!route.meta.hideHeader"/>
|
||||||
|
<router-view/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
|
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 831 B |
After Width: | Height: | Size: 370 B |
After Width: | Height: | Size: 335 B |
After Width: | Height: | Size: 184 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 231 KiB |
After Width: | Height: | Size: 34 KiB |
@ -0,0 +1,195 @@
|
|||||||
|
: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);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
<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>
|
@ -0,0 +1,156 @@
|
|||||||
|
<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>
|
@ -0,0 +1,26 @@
|
|||||||
|
<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>
|
@ -0,0 +1,41 @@
|
|||||||
|
<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>
|
@ -0,0 +1,63 @@
|
|||||||
|
<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>
|
@ -0,0 +1,90 @@
|
|||||||
|
<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>
|
@ -0,0 +1,133 @@
|
|||||||
|
<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>
|
@ -0,0 +1,84 @@
|
|||||||
|
<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>
|
@ -0,0 +1,34 @@
|
|||||||
|
<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>
|
@ -0,0 +1,60 @@
|
|||||||
|
<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>
|
@ -0,0 +1,27 @@
|
|||||||
|
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,5 +1,34 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import './style.css'
|
import '@/assets/style/style.css'
|
||||||
import App from './App.vue'
|
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'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
|
||||||
|
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')
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
import { setupWorker } from 'msw/browser';
|
||||||
|
import { handlers } from './handlers.ts';
|
||||||
|
|
||||||
|
export const worker = setupWorker(...handlers);
|
@ -0,0 +1,116 @@
|
|||||||
|
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 });
|
||||||
|
}),
|
||||||
|
];
|
@ -0,0 +1,122 @@
|
|||||||
|
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,79 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,149 @@
|
|||||||
|
// 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 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
@ -0,0 +1,41 @@
|
|||||||
|
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();
|
||||||
|
}
|
@ -0,0 +1,350 @@
|
|||||||
|
<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>
|
@ -0,0 +1,299 @@
|
|||||||
|
<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>
|
@ -0,0 +1,111 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 73 KiB |
After Width: | Height: | Size: 224 KiB |
After Width: | Height: | Size: 207 KiB |
After Width: | Height: | Size: 148 KiB |
After Width: | Height: | Size: 181 KiB |
After Width: | Height: | Size: 104 KiB |
After Width: | Height: | Size: 102 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 125 KiB |
After Width: | Height: | Size: 99 KiB |
After Width: | Height: | Size: 117 KiB |
After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 222 KiB |
After Width: | Height: | Size: 146 KiB |
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 179 KiB |
After Width: | Height: | Size: 111 KiB |
After Width: | Height: | Size: 122 KiB |
After Width: | Height: | Size: 128 KiB |