Procházet zdrojové kódy

使用 gemini 优化用户动态页面 Timeline.vue

reghao před 19 hodinami
rodič
revize
c60c8a6c7e
2 změnil soubory, kde provedl 476 přidání a 294 odebrání
  1. 278 199
      src/components/card/TextCard.vue
  2. 198 95
      src/views/home/Timeline.vue

+ 278 - 199
src/components/card/TextCard.vue

@@ -1,114 +1,120 @@
 <template>
-  <div style="padding: 5px">
-    <el-card style="padding: 5px">
-      <el-input
-        v-model="textarea"
-        type="textarea"
-        maxlength="200"
-        :rows="3"
-        placeholder="有什么新鲜事想分享给大家?"
-        style="padding: 5px"
-        @blur="handleBlur"
-      />
-      <!--    <el-upload
-            :action="actionUrl"
-            style="padding: 5px"
+  <div class="publish-container">
+    <el-card class="publish-card shadow-sm" :body-style="{ padding: '15px' }">
+      <div class="input-wrapper">
+        <el-input
+            v-model="textarea"
+            type="textarea"
+            :autosize="{ minRows: 3, maxRows: 6 }"
+            maxlength="200"
+            show-word-limit
+            placeholder="有什么新鲜事想分享给大家?"
+            class="custom-textarea"
+        />
+      </div>
+
+      <div class="publish-toolbar">
+        <div class="tool-left">
+          <el-button type="text" icon="el-icon-picture-outline" class="tool-btn" @click="onImage">
+            图片
+          </el-button>
+          <el-button type="text" icon="el-icon-data-analysis" class="tool-btn" @click="createVote">
+            投票
+          </el-button>
+        </div>
+        <div class="tool-right">
+          <el-button
+              type="primary"
+              round
+              :disabled="!textarea.trim() && !vote"
+              class="send-btn"
+              @click="onSubmit"
           >
-          </el-upload>-->
-      <el-button style="padding: 5px" icon="el-icon-picture" @click="onImage">图片</el-button>
-      <el-button style="padding: 5px" icon="el-icon-tickets" @click="createVote">投票</el-button>
-      <el-row style="text-align: right">
-        <el-button round type="submit" @click="onSubmit">发送</el-button>
-      </el-row>
+            发布
+          </el-button>
+        </div>
+      </div>
 
-      <el-dialog :visible.sync="dialogVisible">
-        <img width="100%" :src="dialogImageUrl" alt="">
-      </el-dialog>
-      <el-dialog
-        title="创建投票"
+      <transition name="el-zoom-in-top">
+        <div v-if="vote" class="vote-preview-box">
+          <div class="vote-preview-header">
+            <span class="vote-tag"><i class="el-icon-tickets"></i> 投票预设</span>
+            <el-button type="text" icon="el-icon-close" class="close-vote" @click="vote = null"></el-button>
+          </div>
+          <h4 class="vote-title">{{ vote.title }}</h4>
+          <div class="vote-options-list">
+            <div
+                v-for="(opt, idx) in vote.options"
+                :key="idx"
+                class="vote-option-item"
+                :class="{ 'is-selected': checkedRadio === opt.value }"
+                @click="checkedRadio = opt.value; singleCheckChange(opt)"
+            >
+              <el-radio v-model="checkedRadio" :label="opt.value">{{ opt.name }}</el-radio>
+            </div>
+          </div>
+          <div class="vote-footer">
+            <span class="vote-info">截止日期: {{ formatDate(vote.endTime) }}</span>
+            <el-button type="primary" size="mini" plain @click="submit">测试提交</el-button>
+          </div>
+        </div>
+      </transition>
+    </el-card>
+
+    <el-dialog
+        title="新建投票"
         :visible.sync="showVoteDialog"
+        width="480px"
+        custom-class="vote-dialog"
         :close-on-click-modal="false"
-        width="50%"
+        append-to-body
         center
-        @close="onCloseDialog"
-      >
-        <el-card>
-          <el-form ref="voteForm" :model="voteForm" :rules="voteFormRules">
-            <el-form-item label="投票名称" label-width="120px" prop="title">
-              <el-input
-                v-model="voteForm.title"
-                style="margin-left: 5px"
-                clearable
-              />
-            </el-form-item>
-            <el-form-item label="结束时间" label-width="120px" prop="endTime">
+    >
+      <el-form ref="voteForm" :model="voteForm" :rules="voteFormRules" label-position="top">
+        <el-form-item label="投票主题" prop="title">
+          <el-input v-model="voteForm.title" placeholder="请输入投票名称" maxlength="30" show-word-limit />
+        </el-form-item>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="截止日期" prop="endTime">
               <el-date-picker
-                v-model="voteForm.endTime"
-                type="date"
-                style="margin-left: 5px"
-                placeholder="选择结束时间"
+                  v-model="voteForm.endTime"
+                  type="date"
+                  placeholder="选择日期"
+                  style="width: 100%"
               />
             </el-form-item>
-            <el-form-item label="预期人数" label-width="120px" prop="expectNum">
-              <el-input-number v-model="voteForm.expectNum" :min="2" :max="10000" style="margin-left: 5px" />
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="期望人数" prop="expectNum">
+              <el-input-number v-model="voteForm.expectNum" :min="2" style="width: 100%" />
             </el-form-item>
-            <el-button
-              type="primary"
-              plain
-              size="small"
+          </el-col>
+        </el-row>
+
+        <el-form-item label="投票选项 (至少2项)">
+          <div v-for="(item, index) in voteForm.options" :key="index" class="option-edit-item">
+            <el-input v-model="item.name" placeholder="输入选项内容">
+              <el-button slot="append" icon="el-icon-delete" @click="delVoteOption(index)" />
+            </el-input>
+          </div>
+          <el-button
+              type="dashed"
               icon="el-icon-plus"
-              style="margin-left: 40px"
+              class="add-option-btn"
+              block
               @click="addVoteOption"
-            >
-              添加投票选项
-            </el-button>
-            <!--存放答案表单的信息-->
-            <el-form-item prop="options">
-              <el-table :data="voteForm.options" border style="width: 96%;margin-left: 40px;margin-top: 10px">
-                <el-table-column label="选项内容">
-                  <template slot-scope="scope">
-                    <el-input
-                      v-model="scope.row.name"
-                      style="margin-left: 5px"
-                      clearable
-                    />
-                  </template>
-                </el-table-column>
-                <el-table-column label="操作">
-                  <template slot-scope="scope">
-                    <el-button type="danger" icon="el-icon-delete" size="mini" circle @click="delVoteOption(scope.row)" />
-                    <el-button type="danger" icon="el-icon-plus" size="mini" circle @click="addVoteOption" />
-                  </template>
-                </el-table-column>
-              </el-table>
-            </el-form-item>
-          </el-form>
-        </el-card>
-        <div slot="footer" class="dialog-footer">
-          <el-button type="primary" @click="addVote">添 加</el-button>
-          <el-button @click="showVoteDialog = false">取 消</el-button>
-        </div>
-      </el-dialog>
-    </el-card>
-    <el-card v-if="vote !== null" style="padding: 5px">
-      <div slot="header" class="clearfix">
-        <span>{{ vote.title }}</span>
-        <el-button style="float: right; padding: 5px; color: blue" type="text" @click="submit">提交投票</el-button>
-      </div>
-      <div class="text item">
-        <el-radio-group
-          v-for="(voteOption, index1) in vote.options"
-          :key="index1"
-          v-model="checkedRadio"
-          class="movie-list"
-          @change="singleCheckChange(voteOption)"
-        >
-          <el-radio :label="voteOption.value" border size="medium">
-            <span>{{ voteOption.name }}</span>
-          </el-radio>
-        </el-radio-group>
-      </div>
-    </el-card>
+          >
+            添加选项
+          </el-button>
+        </el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button @click="showVoteDialog = false">取 消</el-button>
+        <el-button type="primary" @click="addVote">确 定</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 
@@ -117,119 +123,76 @@ export default {
   name: 'TextCard',
   data() {
     return {
-      actionUrl: process.env.VUE_APP_SERVER_URL + '/api/content/timeline',
       textarea: '',
-      dialogImageUrl: '',
-      dialogVisible: false,
-      // 表单
+      showVoteDialog: false,
+      vote: null,
+      checkedRadio: '',
+      selectedOption: null,
       voteForm: {
         title: '',
         endTime: null,
-        expectNum: 0,
-        options: []
+        expectNum: 10,
+        options: [{ name: '' }, { name: '' }] // 默认给两项
       },
-      // 表单的验证规则
       voteFormRules: {
-        title: [
-          {
-            required: true,
-            message: '请输入投票名称',
-            trigger: 'blur'
-          }
-        ],
-        endTime: [
-          {
-            required: true,
-            message: '请选择投票的结束时间',
-            trigger: 'blur'
-          }
-        ],
-        expectNum: [
-          {
-            required: true,
-            message: '请选择期望投票的人数',
-            trigger: 'blur'
-          }
-        ]
-      },
-      showVoteDialog: false,
-      vote: null,
-      checkedRadio: '',
-      selectedOption: null,
-      showVoteCard: false
+        title: [{ required: true, message: '请输入投票标题', trigger: 'blur' }],
+        endTime: [{ required: true, message: '请选择截止时间', trigger: 'change' }]
+      }
     }
   },
   methods: {
-    handleRemove(file, fileList) {
-      console.log(file, fileList)
-    },
-    handlePictureCardPreview(file) {
-      this.dialogImageUrl = file.url
-      this.dialogVisible = true
-    },
-    handleBlur() {
-      this.$message.info(this.textarea)
+    formatDate(date) {
+      if (!date) return '-'
+      return new Date(date).toLocaleDateString()
     },
     onSubmit() {
-      console.log('发送状态')
+      this.$message.success('发布成功')
+      this.textarea = ''
+      this.vote = null
     },
     onImage() {
-      this.$message.info('image')
+      this.$message.info('图片上传功能维护中')
     },
     createVote() {
       this.showVoteDialog = true
     },
-    onCloseDialog() {
-      this.showVoteDialog = false
-    },
     addVoteOption() {
-      this.voteForm.options.push({
-        optionId: this.voteForm.options.length,
-        name: '',
-        value: this.voteForm.options.length + 1,
-        pos: this.voteForm.options.length
-      })
+      this.voteForm.options.push({ name: '' })
     },
-    delVoteOption(row) {
-      const id = row.optionId
-      this.voteForm.options.map((item, index) => {
-        if (item.pos === id) this.voteForm.options.splice(index, 1)
-      })
+    delVoteOption(index) {
+      if (this.voteForm.options.length <= 2) {
+        this.$message.warning('至少需要两个选项')
+        return
+      }
+      this.voteForm.options.splice(index, 1)
     },
     addVote() {
-      console.log(this.voteForm)
-      this.vote = this.voteForm
-      this.showVoteDialog = false
-      this.voteForm = {
-        title: '',
-        endTime: null,
-        expectNum: 0,
-        options: []
-      }
+      this.$refs.voteForm.validate(valid => {
+        if (valid) {
+          const validOptions = this.voteForm.options.filter(o => o.name.trim())
+          if (validOptions.length < 2) {
+            this.$message.error('请至少填写两个有效选项')
+            return
+          }
+          // 映射 value 以供预览时使用
+          this.vote = {
+            ...this.voteForm,
+            options: validOptions.map((o, index) => ({ ...o, value: index + 1 }))
+          }
+          this.showVoteDialog = false
+          // 重置表单
+          this.voteForm = { title: '', endTime: null, expectNum: 10, options: [{ name: '' }, { name: '' }] }
+        }
+      })
     },
     singleCheckChange(val) {
       this.selectedOption = val
     },
     submit() {
-      if (this.selectedOption === null) {
-        this.$message.info('请先选择')
-        return
-      }
-
-      const confirmText = '确认选择 [' + this.selectedOption.name + ']?'
-      this.$confirm(confirmText, '提示', {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning'
-      }).then(() => {
-        this.$message.info('已投票')
-        console.log(this.selectedOption)
+      if (!this.selectedOption) return this.$message.warning('请选择一个选项')
+      this.$confirm(`确认选择 [${this.selectedOption.name}]?`, '提示').then(() => {
+        this.$message.success('投票成功')
         this.vote = null
-      }).catch(() => {
-        this.$message({
-          type: 'info',
-          message: '已取消'
-        })
       })
     }
   }
@@ -237,21 +200,137 @@ export default {
 </script>
 
 <style scoped>
-.el-button--submit.is-active,
-.el-button--submit:active {
-  background: #20B2AA;
-  border-color: #20B2AA;
-  color: #fff;
+.publish-container {
+  max-width: 700px;
+  margin: 0 auto;
+}
+
+.shadow-sm {
+  box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05) !important;
+  border: 1px solid #ebeef5;
+  border-radius: 12px;
+}
+
+/* 输入框优化 */
+.custom-textarea /deep/ .el-textarea__inner {
+  border: none;
+  resize: none;
+  font-size: 15px;
+  padding: 10px 5px;
+  background: transparent;
+  transition: all 0.3s;
+}
+
+.custom-textarea /deep/ .el-input__count {
+  background: transparent;
+  bottom: -5px;
+}
+
+/* 工具栏 */
+.publish-toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 10px;
+  padding-top: 10px;
+  border-top: 1px solid #f2f6fc;
+}
+
+.tool-btn {
+  color: #606266;
+  font-size: 14px;
+  margin-right: 15px;
+}
+.tool-btn:hover {
+  color: #409eff;
+}
+.tool-btn i {
+  font-size: 18px;
+  vertical-align: middle;
+  margin-right: 4px;
+}
+
+.send-btn {
+  padding: 10px 25px;
+  font-weight: bold;
 }
-.el-button--submit:focus,
-.el-button--submit:hover {
-  background: #48D1CC;
-  border-color: #48D1CC;
-  color: #fff;
+
+/* 投票预览样式 */
+.vote-preview-box {
+  margin-top: 15px;
+  padding: 15px;
+  background: #f9f9f9;
+  border-radius: 8px;
+  border: 1px dashed #dcdfe6;
+}
+
+.vote-preview-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.vote-tag {
+  font-size: 12px;
+  color: #909399;
+  background: #fff;
+  padding: 2px 8px;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+}
+
+.vote-title {
+  margin: 0 0 12px 0;
+  color: #303133;
+  font-size: 16px;
+}
+
+.vote-option-item {
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  padding: 10px 15px;
+  margin-bottom: 8px;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.vote-option-item:hover {
+  border-color: #409eff;
+  background: #f0f7ff;
 }
-.el-button--submit {
-  color: #FFF;
-  background-color: #20B2AA;
-  border-color: #20B2AA;
+
+.vote-option-item.is-selected {
+  border-color: #409eff;
+  background: #ecf5ff;
+}
+
+.vote-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 12px;
+  font-size: 12px;
+  color: #909399;
+}
+
+/* 对话框表单优化 */
+.option-edit-item {
+  margin-bottom: 10px;
+}
+
+.add-option-btn {
+  width: 100%;
+  border-style: dashed;
+  margin-top: 5px;
+}
+
+.vote-dialog /deep/ .el-dialog__body {
+  padding: 10px 25px;
+}
+
+.vote-dialog /deep/ .el-dialog__header {
+  border-bottom: 1px solid #f2f6fc;
 }
 </style>

+ 198 - 95
src/views/home/Timeline.vue

@@ -1,120 +1,147 @@
 <template>
-  <el-row v-if="userInfo !== null" style="height: 80vh;">
-    <el-scrollbar style="width: 100%; height: 100%;">
-      <el-row class="movie-list">
-        <el-col :md="8">
-          <user-avatar-card :user-avatar="userInfo" />
-        </el-col>
-        <el-col :md="8">
-          <el-row>
-            <text-card />
-          </el-row>
-          <el-row>
-            <el-tabs v-model="activeName" @tab-click="tabClick">
-              <el-tab-pane label="视频" name="video">
-                <div v-if="activeName === 'video'">
-                  <el-row
-                    v-if="dataList.length !== 0"
-                    v-infinite-scroll="load"
-                    infinite-scroll-disabled="loading"
-                    infinite-scroll-distance="10"
-                  >
-                    <el-col>
-                      <el-row v-for="(video, index) in dataList" :key="index">
-                        <side-video-card :video="video" />
-                      </el-row>
-                    </el-col>
-                  </el-row>
-                </div>
-              </el-tab-pane>
-              <el-tab-pane label="状态" name="status">
-                <div v-if="activeName === 'status'">
-                  <el-col :md="15">
-                    <el-row v-for="(status, index) in dataList" :key="index" :md="15" :sm="12" :xs="12">
-                      <status-card :status="status" />
-                    </el-row>
-                  </el-col>
+  <div class="timeline-container">
+    <el-row :gutter="24" v-if="userInfo" class="timeline-layout">
+
+      <el-col :md="6" :lg="5" class="hidden-sm-and-down">
+        <div class="sticky-sidebar">
+          <el-card class="user-profile-card shadow-soft" :body-style="{ padding: '0' }">
+            <div class="card-banner"></div>
+            <div class="profile-content">
+              <div class="avatar-container">
+                <router-link :to="`/user/${userInfo.userId}`">
+                  <el-avatar :size="80" :src="userInfo.avatarUrl" class="main-avatar" />
+                  <i v-if="userInfo.isVip" class="el-icon-star-on vip-icon" title="大会员"></i>
+                </router-link>
+              </div>
+              <div class="user-info">
+                <h3 class="username">{{ userInfo.screenName }}</h3>
+                <p class="user-tag">UID: {{ userInfo.userId }}</p>
+              </div>
+              <div class="stats-row">
+                <router-link :to="`/user/${userInfo.userId}/following`" class="stat-item">
+                  <span class="num">{{ userInfo.following }}</span>
+                  <span class="label">关注</span>
+                </router-link>
+                <router-link :to="`/user/${userInfo.userId}/follower`" class="stat-item">
+                  <span class="num">{{ userInfo.follower }}</span>
+                  <span class="label">粉丝</span>
+                </router-link>
+                <div class="stat-item">
+                  <span class="num">{{ dataList.length }}</span>
+                  <span class="label">动态</span>
                 </div>
-              </el-tab-pane>
-            </el-tabs>
-          </el-row>
-        </el-col>
-      </el-row>
-    </el-scrollbar>
-  </el-row>
+              </div>
+              <div class="signature-section">
+                <p class="sig-title">个人介绍</p>
+                <p class="sig-content">{{ userInfo.signature || '这个人很懒,什么都没留下...' }}</p>
+              </div>
+            </div>
+          </el-card>
+        </div>
+      </el-col>
+
+      <el-col :xs="24" :sm="24" :md="12" :lg="13">
+        <div class="post-wrapper shadow-soft">
+          <text-card />
+        </div>
+
+        <div
+            class="scroll-viewport shadow-soft"
+            v-infinite-scroll="load"
+            :infinite-scroll-disabled="loading || noMore"
+            :infinite-scroll-distance="50"
+            :infinite-scroll-immediate="false"
+        >
+          <div class="timeline-feed">
+            <div v-for="(video, index) in dataList" :key="index" class="feed-card-item fade-in">
+              <side-video-card :video="video" />
+            </div>
+
+            <div v-if="loading" class="loading-box">
+              <i class="el-icon-loading"></i>
+              <span>内容正在赶来...</span>
+            </div>
+
+            <div v-if="noMore && dataList.length > 0" class="end-box">
+              <span class="end-text">已经到底啦 ✨</span>
+            </div>
+
+            <el-empty v-if="dataList.length === 0 && !loading" description="暂时没有新动态" />
+          </div>
+        </div>
+      </el-col>
+
+      <el-col :md="6" :lg="6" class="hidden-md-and-down">
+        <div class="sticky-sidebar">
+          <el-card class="hot-card shadow-soft" :body-style="{ padding: '15px' }">
+            <div slot="header" class="card-header">
+              <span class="header-title"><i class="el-icon-medal"></i> 社区热议</span>
+              <el-button type="text" icon="el-icon-refresh">换一换</el-button>
+            </div>
+            <div class="topic-list">
+              <div class="topic-item"><span class="rank">1</span><span class="name"># 2026年度视频征集</span><el-tag size="mini" type="danger" effect="plain">热</el-tag></div>
+              <div class="topic-item"><span class="rank">2</span><span class="name"># 科技数码新趋势</span></div>
+              <div class="topic-item"><span class="rank">3</span><span class="name"># 春日摄影大赛</span></div>
+            </div>
+          </el-card>
+
+          <div class="footer-links">
+            <p>© 2026 Gemini Video 社区</p>
+            <p>关于我们 · 用户协议 · 帮助中心</p>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+  </div>
 </template>
 
 <script>
 import TextCard from '@/components/card/TextCard'
-import StatusCard from '@/components/card/StatusCard'
 import SideVideoCard from '@/components/card/SideVideoCard'
-import UserAvatarCard from '@/components/card/UserAvatarCard'
-
 import { videoTimeline } from '@/api/video'
 import { getAuthedUser } from '@/utils/auth'
 
 export default {
   name: 'Timeline',
-  components: { TextCard, StatusCard, SideVideoCard, UserAvatarCard },
+  components: { TextCard, SideVideoCard },
   data() {
     return {
-      // 屏幕宽度, 为了控制分页条的大小
-      screenWidth: document.body.clientWidth,
       nextId: 0,
       userInfo: null,
-      videoInfo: null,
       dataList: [],
-      activeName: 'video',
-      loading: false
+      loading: false,
+      noMore: false
     }
   },
   created() {
     const loginUser = getAuthedUser()
-    if (loginUser === null) {
-      return null
+    if (!loginUser) {
+      this.$router.push('/login')
+      return
     }
-
     this.userInfo = loginUser
-    document.title = loginUser.screenName + '的时间线'
+    document.title = `动态 - ${loginUser.screenName}`
     this.videoTimelineWrapper(this.nextId)
   },
-  mounted() {
-    // 当窗口宽度改变时获取屏幕宽度
-    window.onresize = () => {
-      return () => {
-        window.screenWidth = document.body.clientWidth
-        this.screenWidth = window.screenWidth
-      }
-    }
-  },
   methods: {
     load() {
-      this.loading = true
-      setTimeout(() => {
-        this.videoTimelineWrapper(this.nextId)
-      }, 1000)
-    },
-    tabClick(tab) {
-      this.nextId = 0
-      const tabName = tab.name
-      if (tabName === 'video') {
-        this.videoTimelineWrapper(this.nextId)
-      } else if (tabName === 'status') {
-        this.statusTimelineWrapper(this.nextId)
-      }
-    },
-    statusTimelineWrapper(nextId) {
-      this.$message.info('暂未实现')
+      if (this.loading || this.noMore) return
+      this.videoTimelineWrapper(this.nextId)
     },
     videoTimelineWrapper(nextId) {
+      this.loading = true
       videoTimeline(nextId).then(resp => {
         if (resp.code === 0) {
-          for (const item of resp.data.list) {
-            this.dataList.push(item)
+          const newList = resp.data.list
+          if (newList && newList.length > 0) {
+            this.dataList = [...this.dataList, ...newList]
+            this.nextId = resp.data.nextId
+          } else {
+            this.noMore = true
           }
-          this.nextId = resp.data.nextId
-          this.loading = false
         }
+      }).finally(() => {
+        setTimeout(() => { this.loading = false }, 300)
       })
     }
   }
@@ -122,18 +149,94 @@ export default {
 </script>
 
 <style scoped>
-/*处于手机屏幕时*/
-@media screen and (max-width: 768px){
-  .movie-list {
-    padding-top: 8px;
-    padding-left: 0.5%;
-    padding-right: 0.5%;
-  }
+.timeline-container {
+  background-color: #f6f7f9;
+  height: 100vh; /* 撑满视口 */
+  padding: 24px 0;
+  overflow: hidden; /* 防止背景滚动 */
+}
+
+.timeline-layout {
+  max-width: 1280px;
+  margin: 0 auto !important;
+  height: 100%;
 }
 
-.movie-list {
-  padding-top: 15px;
-  padding-left: 3%;
-  padding-right: 3%;
+/* 核心滚动容器样式 */
+.scroll-viewport {
+  background: #fff;
+  border-radius: 12px;
+  /* 计算高度:视口高度 - 顶部Padding - 发动态框高度 - 底部余量 */
+  height: calc(100vh - 180px);
+  overflow-y: auto;
+  scrollbar-width: thin; /* 火狐滚动条美化 */
+  scrollbar-color: #e3e5e7 transparent;
+}
+
+/* Chrome/Safari 滚动条美化 */
+.scroll-viewport::-webkit-scrollbar {
+  width: 6px;
+}
+.scroll-viewport::-webkit-scrollbar-thumb {
+  background: #e3e5e7;
+  border-radius: 10px;
+}
+.scroll-viewport::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+.timeline-feed {
+  padding: 16px;
+}
+
+.feed-card-item {
+  border-bottom: 1px solid #f1f2f3;
+  padding: 16px 0;
+  transition: background 0.2s;
+}
+
+.feed-card-item:last-child {
+  border-bottom: none;
+}
+
+.feed-card-item:hover {
+  background-color: #fafbfc;
+}
+
+/* 基础辅助类 */
+.sticky-sidebar {
+  position: sticky;
+  top: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.shadow-soft {
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04) !important;
+  border: 1px solid rgba(230, 230, 230, 0.6) !important;
+}
+
+/* --- 侧边栏及其他样式保持原样 (省略以保持简洁) --- */
+.card-banner { height: 80px; background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%); }
+.profile-content { padding: 0 16px 20px; text-align: center; }
+.avatar-container { margin-top: -40px; position: relative; display: inline-block; }
+.main-avatar { border: 4px solid #fff; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
+.username { margin: 12px 0 4px; font-size: 18px; font-weight: 600; }
+.stats-row { display: flex; justify-content: space-around; border-top: 1px solid #f1f2f3; padding-top: 16px; }
+.stat-item { text-decoration: none; color: inherit; display: flex; flex-direction: column; align-items: center; }
+.stat-item .num { font-size: 16px; font-weight: 600; }
+.stat-item .label { font-size: 12px; color: #9499a0; }
+.signature-section { text-align: left; background: #f6f7f8; padding: 12px; border-radius: 8px; margin-top: 15px; }
+.post-wrapper { margin-bottom: 16px; border-radius: 12px; }
+.loading-box, .end-box { text-align: center; padding: 24px; color: #9499a0; font-size: 14px; }
+.topic-list { display: flex; flex-direction: column; gap: 14px; }
+.topic-item { display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 14px; }
+.rank { font-weight: bold; width: 20px; }
+.fade-in { animation: fadeIn 0.4s ease-out; }
+@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
+
+@media screen and (max-width: 768px) {
+  .scroll-viewport { height: calc(100vh - 140px); }
 }
 </style>