Browse Source

基础信息修改

flower_bs 4 months ago
parent
commit
5e2dcfe455
68 changed files with 1065 additions and 268 deletions
  1. BIN
      media/avatars/0f2c0a3dad2a4522a7caf9e32d5f393c.png
  2. BIN
      media/avatars/1d2382f9bedc4d239d66edec3e5ac7f6.png
  3. BIN
      media/avatars/278e8a08e1c247c28074323bb4be890b.png
  4. BIN
      media/avatars/2807e8915ff54636b8df44d029aaea5b.png
  5. BIN
      media/avatars/36c1eb7c34a04e0a99c9decb2bcafba1.png
  6. BIN
      media/avatars/42d8f7dfadda416487f5b4a8e8db1259.png
  7. BIN
      media/avatars/5a25d20953ce478f8b4e654cef1925b4.png
  8. BIN
      media/avatars/5ce21eda2da24d35b122113a66f6a4fc.png
  9. BIN
      media/avatars/6667b6e321784f829185b58bcba1a9a6.png
  10. BIN
      media/avatars/6d07ded24bbc4bc1bda0cd33ce000808.png
  11. BIN
      media/avatars/6ebb3ae2d54d45a291af3b44d9ec22bd.png
  12. BIN
      media/avatars/71b51cf5699c415d9310c9357c8353b1.png
  13. BIN
      media/avatars/8347229e81c443ec857b85f734140456.png
  14. BIN
      media/avatars/a5b3c26947ff4667ae093a87e7615bd4.png
  15. BIN
      media/avatars/b0dda629b6c547f6a35d1af0bc6219b8.png
  16. BIN
      media/avatars/bd7d8a712e1b41c9a79dc47ef3e3b469.png
  17. BIN
      media/avatars/c270de75f9214c7cbdd3063185bd21f9.png
  18. BIN
      media/avatars/ddfe749db3b048f5ae6a440397ea157f.png
  19. BIN
      media/avatars/e8cf6af5ff7b45379762b9692dc463c8.png
  20. BIN
      media/avatars/eac9a7bd0c414733b66e1a45eeb52fb8.png
  21. BIN
      media/avatars/f1d3a120455b4a8a8b9f79879d74e940.png
  22. BIN
      media/avatars/f2b877c6e063404abf9854653170660a.png
  23. BIN
      media/avatars/f46c84eb2b6b4e5f94ec36457543f58b.png
  24. BIN
      media/avatars/ffceede880e7418cb18560a6db1f18d2.png
  25. BIN
      media/static/img/GreaterWMS.png
  26. BIN
      media/static/img/GreaterWMS_en.png
  27. BIN
      media/static/img/alipay.jpg
  28. BIN
      media/static/img/contact.png
  29. BIN
      media/static/img/dongtai.png
  30. BIN
      media/static/img/dongtai1.png
  31. BIN
      media/static/img/github.png
  32. BIN
      media/static/img/logo.png
  33. BIN
      media/static/img/logout.png
  34. BIN
      media/static/img/mobile_dn.jpg
  35. BIN
      media/static/img/mobile_dn_en.jpg
  36. BIN
      media/static/img/mobile_equ.jpg
  37. BIN
      media/static/img/mobile_equ_en.jpg
  38. BIN
      media/static/img/mobile_splash.jpg
  39. BIN
      media/static/img/money.png
  40. BIN
      media/static/img/photo.png
  41. BIN
      media/static/img/profile.png
  42. BIN
      media/static/img/register.png
  43. BIN
      media/static/img/user.jpg
  44. BIN
      media/static/img/user.png
  45. BIN
      media/static/img/video.png
  46. BIN
      media/static/img/wechat.jpg
  47. 2 1
      requirements.txt
  48. BIN
      static/img/Katie.jpg
  49. 119 78
      templates/src/boot/axios_request.js
  50. 1 1
      templates/src/components/Breadcrumb/index.vue
  51. 88 59
      templates/src/components/ImageCropper/index.vue
  52. 118 0
      templates/src/components/date/index.js
  53. 3 0
      templates/src/store/getters.js
  54. 22 4
      templates/src/store/modules/user.js
  55. 6 2
      templates/src/views/components-demo/avatar-upload.vue
  56. 4 4
      templates/src/views/login/index.vue
  57. 412 13
      templates/src/views/profile/components/Account.vue
  58. 15 12
      templates/src/views/profile/components/UserCard.vue
  59. 19 14
      templates/src/views/profile/index.vue
  60. 3 1
      userprofile/filter.py
  61. 13 6
      userprofile/migrations/0001_initial.py
  62. 0 27
      userprofile/migrations/0002_alter_academicprofile_user_and_more.py
  63. 0 17
      userprofile/migrations/0003_alter_academicprofile_table.py
  64. 34 2
      userprofile/models.py
  65. 24 2
      userprofile/serializers.py
  66. 4 2
      userprofile/urls.py
  67. 166 15
      userprofile/views.py
  68. 12 8
      userregister/views.py

BIN
media/avatars/0f2c0a3dad2a4522a7caf9e32d5f393c.png


BIN
media/avatars/1d2382f9bedc4d239d66edec3e5ac7f6.png


BIN
media/avatars/278e8a08e1c247c28074323bb4be890b.png


BIN
media/avatars/2807e8915ff54636b8df44d029aaea5b.png


BIN
media/avatars/36c1eb7c34a04e0a99c9decb2bcafba1.png


BIN
media/avatars/42d8f7dfadda416487f5b4a8e8db1259.png


BIN
media/avatars/5a25d20953ce478f8b4e654cef1925b4.png


BIN
media/avatars/5ce21eda2da24d35b122113a66f6a4fc.png


BIN
media/avatars/6667b6e321784f829185b58bcba1a9a6.png


BIN
media/avatars/6d07ded24bbc4bc1bda0cd33ce000808.png


BIN
media/avatars/6ebb3ae2d54d45a291af3b44d9ec22bd.png


BIN
media/avatars/71b51cf5699c415d9310c9357c8353b1.png


BIN
media/avatars/8347229e81c443ec857b85f734140456.png


BIN
media/avatars/a5b3c26947ff4667ae093a87e7615bd4.png


BIN
media/avatars/b0dda629b6c547f6a35d1af0bc6219b8.png


BIN
media/avatars/bd7d8a712e1b41c9a79dc47ef3e3b469.png


BIN
media/avatars/c270de75f9214c7cbdd3063185bd21f9.png


BIN
media/avatars/ddfe749db3b048f5ae6a440397ea157f.png


BIN
media/avatars/e8cf6af5ff7b45379762b9692dc463c8.png


BIN
media/avatars/eac9a7bd0c414733b66e1a45eeb52fb8.png


BIN
media/avatars/f1d3a120455b4a8a8b9f79879d74e940.png


BIN
media/avatars/f2b877c6e063404abf9854653170660a.png


BIN
media/avatars/f46c84eb2b6b4e5f94ec36457543f58b.png


BIN
media/avatars/ffceede880e7418cb18560a6db1f18d2.png


BIN
media/static/img/GreaterWMS.png


BIN
media/static/img/GreaterWMS_en.png


BIN
media/static/img/alipay.jpg


BIN
media/static/img/contact.png


BIN
media/static/img/dongtai.png


BIN
media/static/img/dongtai1.png


BIN
media/static/img/github.png


BIN
media/static/img/logo.png


BIN
media/static/img/logout.png


BIN
media/static/img/mobile_dn.jpg


BIN
media/static/img/mobile_dn_en.jpg


BIN
media/static/img/mobile_equ.jpg


BIN
media/static/img/mobile_equ_en.jpg


BIN
media/static/img/mobile_splash.jpg


BIN
media/static/img/money.png


BIN
media/static/img/photo.png


BIN
media/static/img/profile.png


BIN
media/static/img/register.png


BIN
media/static/img/user.jpg


BIN
media/static/img/user.png


BIN
media/static/img/video.png


BIN
media/static/img/wechat.jpg


+ 2 - 1
requirements.txt

@@ -50,4 +50,5 @@ unicodecsv==0.14.1
 uritemplate==4.1.1
 uritemplate==4.1.1
 urllib3==1.26.12
 urllib3==1.26.12
 zope.interface==6.0
 zope.interface==6.0
-psycopg2-binary==2.9.3
+psycopg2-binary==2.9.3
+pillow==10.4.0

BIN
static/img/Katie.jpg


+ 119 - 78
templates/src/boot/axios_request.js

@@ -115,28 +115,58 @@ const loadingIndicator = {
 function handleApiError(error) {
 function handleApiError(error) {
   let message = '未知错误'
   let message = '未知错误'
 
 
-  if (error.code === 'ECONNABORTED' ||
-      error.message?.includes('timeout') ||
-      error.message === 'Network Error') {
+  if (
+    error.code === 'ECONNABORTED' ||
+    error.message?.includes('timeout') ||
+    error.message === 'Network Error'
+  ) {
     message = '网络连接超时,请检查网络连接'
     message = '网络连接超时,请检查网络连接'
   } else if (!error.response) {
   } else if (!error.response) {
     message = '服务器无响应,请检查网络连接'
     message = '服务器无响应,请检查网络连接'
   } else {
   } else {
     switch (error.response.status) {
     switch (error.response.status) {
-      case 400: message = '请求错误'; break
-      case 401: message = '未授权,请登录'; break
-      case 403: message = '拒绝访问'; break
-      case 404: message = '请求地址出错'; break
-      case 405: message = '不支持的请求方法'; break
-      case 408: message = '请求超时'; break
-      case 409: message = '请求冲突'; break
-      case 410: message = '资源已被删除'; break
-      case 500: message = '服务器内部错误'; break
-      case 501: message = '服务未实现'; break
-      case 502: message = '网关错误'; break
-      case 503: message = '服务不可用'; break
-      case 504: message = '网关超时'; break
-      case 505: message = 'HTTP版本不受支持'; break
+      case 400:
+        message = '请求错误'
+        break
+      case 401:
+        message = '未授权,请登录'
+        break
+      case 403:
+        message = '拒绝访问'
+        break
+      case 404:
+        message = '请求地址出错'
+        break
+      case 405:
+        message = '不支持的请求方法'
+        break
+      case 408:
+        message = '请求超时'
+        break
+      case 409:
+        message = '请求冲突'
+        break
+      case 410:
+        message = '资源已被删除'
+        break
+      case 500:
+        message = '服务器内部错误'
+        break
+      case 501:
+        message = '服务未实现'
+        break
+      case 502:
+        message = '网关错误'
+        break
+      case 503:
+        message = '服务不可用'
+        break
+      case 504:
+        message = '网关超时'
+        break
+      case 505:
+        message = 'HTTP版本不受支持'
+        break
     }
     }
   }
   }
 
 
@@ -166,7 +196,7 @@ function checkAuth() {
 
 
 // 请求拦截器 - 认证实例
 // 请求拦截器 - 认证实例
 axiosInstanceAuth.interceptors.request.use(
 axiosInstanceAuth.interceptors.request.use(
-  config => {
+  (config) => {
     if (!checkAuth()) {
     if (!checkAuth()) {
       loadingIndicator.hide()
       loadingIndicator.hide()
       Bus.$emit('needLogin', true)
       Bus.$emit('needLogin', true)
@@ -181,28 +211,25 @@ axiosInstanceAuth.interceptors.request.use(
 
 
     return config
     return config
   },
   },
-  error => {
+  (error) => {
     loadingIndicator.hide()
     loadingIndicator.hide()
     return Promise.reject(error)
     return Promise.reject(error)
   }
   }
 )
 )
 
 
 // 响应拦截器 - 认证实例
 // 响应拦截器 - 认证实例
-axiosInstanceAuth.interceptors.response.use(
-  response => {
-    if (response.data.detail && response.data.detail !== 'success') {
-      showNotification(response.data.detail, 'error')
-    }
+axiosInstanceAuth.interceptors.response.use((response) => {
+  if (response.data.detail && response.data.detail !== 'success') {
+    showNotification(response.data.detail, 'error')
+  }
 
 
-    loadingIndicator.hide()
-    return response.data
-  },
-  handleApiError
-)
+  loadingIndicator.hide()
+  return response.data
+}, handleApiError)
 
 
 // 请求拦截器 - 认证扫描实例
 // 请求拦截器 - 认证扫描实例
 axiosInstanceAuthScan.interceptors.request.use(
 axiosInstanceAuthScan.interceptors.request.use(
-  config => {
+  (config) => {
     if (!checkAuth()) {
     if (!checkAuth()) {
       loadingIndicator.hide()
       loadingIndicator.hide()
       Bus.$emit('needLogin', true)
       Bus.$emit('needLogin', true)
@@ -217,24 +244,21 @@ axiosInstanceAuthScan.interceptors.request.use(
 
 
     return config
     return config
   },
   },
-  error => {
+  (error) => {
     loadingIndicator.hide()
     loadingIndicator.hide()
     return Promise.reject(error)
     return Promise.reject(error)
   }
   }
 )
 )
 
 
 // 响应拦截器 - 认证扫描实例
 // 响应拦截器 - 认证扫描实例
-axiosInstanceAuthScan.interceptors.response.use(
-  response => {
-    loadingIndicator.hide()
-    return response.data
-  },
-  handleApiError
-)
+axiosInstanceAuthScan.interceptors.response.use((response) => {
+  loadingIndicator.hide()
+  return response.data
+}, handleApiError)
 
 
 // 请求拦截器 - 基础实例
 // 请求拦截器 - 基础实例
 axiosInstance.interceptors.request.use(
 axiosInstance.interceptors.request.use(
-  config => {
+  (config) => {
     config.headers = {
     config.headers = {
       ...config.headers,
       ...config.headers,
       language: lang
       language: lang
@@ -246,28 +270,25 @@ axiosInstance.interceptors.request.use(
 
 
     return config
     return config
   },
   },
-  error => {
+  (error) => {
     loadingIndicator.hide()
     loadingIndicator.hide()
     return Promise.reject(error)
     return Promise.reject(error)
   }
   }
 )
 )
 
 
 // 响应拦截器 - 基础实例
 // 响应拦截器 - 基础实例
-axiosInstance.interceptors.response.use(
-  response => {
-    if (response.data.detail && response.data.detail !== 'success') {
-      showNotification(response.data.detail, 'error')
-    }
+axiosInstance.interceptors.response.use((response) => {
+  if (response.data.detail && response.data.detail !== 'success') {
+    showNotification(response.data.detail, 'error')
+  }
 
 
-    loadingIndicator.hide()
-    return response.data
-  },
-  handleApiError
-)
+  loadingIndicator.hide()
+  return response.data
+}, handleApiError)
 
 
 // 请求拦截器 - 版本检查实例
 // 请求拦截器 - 版本检查实例
 axiosInstanceVersion.interceptors.request.use(
 axiosInstanceVersion.interceptors.request.use(
-  config => {
+  (config) => {
     if (!checkAuth()) {
     if (!checkAuth()) {
       loadingIndicator.hide()
       loadingIndicator.hide()
       Bus.$emit('needLogin', true)
       Bus.$emit('needLogin', true)
@@ -280,37 +301,36 @@ axiosInstanceVersion.interceptors.request.use(
 
 
     return config
     return config
   },
   },
-  error => {
+  (error) => {
     loadingIndicator.hide()
     loadingIndicator.hide()
     return Promise.reject(error)
     return Promise.reject(error)
   }
   }
 )
 )
 
 
 // 响应拦截器 - 版本检查实例
 // 响应拦截器 - 版本检查实例
-axiosInstanceVersion.interceptors.response.use(
-  response => {
-    loadingIndicator.hide()
-    return response.data
-  },
-  handleApiError
-)
+axiosInstanceVersion.interceptors.response.use((response) => {
+  loadingIndicator.hide()
+  return response.data
+}, handleApiError)
 
 
 // 请求拦截器 - 文件实例
 // 请求拦截器 - 文件实例
+// 修改文件上传的拦截器
 axiosFile.interceptors.request.use(
 axiosFile.interceptors.request.use(
-  config => {
+  (config) => {
     if (!checkAuth()) {
     if (!checkAuth()) {
       loadingIndicator.hide()
       loadingIndicator.hide()
       Bus.$emit('needLogin', true)
       Bus.$emit('needLogin', true)
       return Promise.reject(new Error('未登录'))
       return Promise.reject(new Error('未登录'))
     }
     }
 
 
+    // 修改这里:允许自定义Content-Type
+    if (!config.headers['Content-Type']) {
+      config.headers['Content-Type'] = 'application/vnd.ms-excel'
+    }
+
     config.headers = {
     config.headers = {
       ...config.headers,
       ...config.headers,
-      'Content-Type': 'application/vnd.ms-excel',
-      token: localStorage.getItem('openid'),
-      appid: localStorage.getItem('appid'),
-      operator: localStorage.getItem('login_id'),
-      language: lang
+      token: localStorage.getItem('openid')
     }
     }
 
 
     if (['post', 'patch', 'put', 'delete'].includes(config.method)) {
     if (['post', 'patch', 'put', 'delete'].includes(config.method)) {
@@ -319,24 +339,21 @@ axiosFile.interceptors.request.use(
 
 
     return config
     return config
   },
   },
-  error => {
+  (error) => {
     loadingIndicator.hide()
     loadingIndicator.hide()
     return Promise.reject(error)
     return Promise.reject(error)
   }
   }
 )
 )
 
 
 // 响应拦截器 - 文件实例
 // 响应拦截器 - 文件实例
-axiosFile.interceptors.response.use(
-  response => {
-    if (response.data.detail && response.data.detail !== 'success') {
-      showNotification(response.data.detail, 'error')
-    }
+axiosFile.interceptors.response.use((response) => {
+  if (response.data.detail && response.data.detail !== 'success') {
+    showNotification(response.data.detail, 'error')
+  }
 
 
-    loadingIndicator.hide()
-    return response
-  },
-  handleApiError
-)
+  loadingIndicator.hide()
+  return response
+}, handleApiError)
 
 
 // API 方法
 // API 方法
 export const getauth = (url) => axiosInstanceAuth.get(url)
 export const getauth = (url) => axiosInstanceAuth.get(url)
@@ -349,8 +366,31 @@ export const patchauth = (url, data) => axiosInstanceAuth.patch(url, data)
 export const deleteauth = (url) => axiosInstanceAuth.delete(url)
 export const deleteauth = (url) => axiosInstanceAuth.delete(url)
 export const viewPrintAuth = (url) => axiosInstanceAuth.get(url)
 export const viewPrintAuth = (url) => axiosInstanceAuth.get(url)
 export const scangetauth = (url) => axiosInstanceAuthScan.get(url)
 export const scangetauth = (url) => axiosInstanceAuthScan.get(url)
-export const scanpostauth = (url, data) => axiosInstanceAuthScan.post(url, data)
+export const scanpostauth = (url, data) =>
+  axiosInstanceAuthScan.post(url, data)
 export const getfile = (url) => axiosFile.get(url)
 export const getfile = (url) => axiosFile.get(url)
+// 修改 postfileauth 函数
+export const postfileauth = (url, data, config = {}) => {
+  // 创建合并配置
+  const mergedConfig = {
+    ...config,
+    headers: {
+      // 保留默认配置
+      token: localStorage.getItem('openid'),
+
+      // 覆盖自定义配置
+      ...(config.headers || {})
+    }
+  }
+
+  // 特殊处理:如果是 FormData,自动设置正确的 Content-Type
+  if (data instanceof FormData) {
+    mergedConfig.headers['Content-Type'] = 'multipart/form-data'
+  }
+
+  console.log('[postfileauth] config', mergedConfig)
+  return axiosFile.post(url, data, mergedConfig)
+}
 
 
 // 挂载到 Vue 原型
 // 挂载到 Vue 原型
 Vue.prototype.$axios = axios
 Vue.prototype.$axios = axios
@@ -369,5 +409,6 @@ export default {
   viewPrintAuth,
   viewPrintAuth,
   getfile,
   getfile,
   scangetauth,
   scangetauth,
-  scanpostauth
+  scanpostauth,
+  postfileauth
 }
 }

+ 1 - 1
templates/src/components/Breadcrumb/index.vue

@@ -37,7 +37,7 @@ export default {
       const first = matched[0]
       const first = matched[0]
 
 
       if (!this.isDashboard(first)) {
       if (!this.isDashboard(first)) {
-        matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
+        matched = [{ path: '/dashboard', meta: { title: '主页' }}].concat(matched)
       }
       }
 
 
       this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
       this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)

+ 88 - 59
templates/src/components/ImageCropper/index.vue

@@ -20,8 +20,16 @@
             <i class="vicp-icon1-bottom" />
             <i class="vicp-icon1-bottom" />
           </i>
           </i>
           <span v-show="loading !== 1" class="vicp-hint">{{ lang.hint }}</span>
           <span v-show="loading !== 1" class="vicp-hint">{{ lang.hint }}</span>
-          <span v-show="!isSupported" class="vicp-no-supported-hint">{{ lang.noSupported }}</span>
-          <input v-show="false" v-if="step == 1" ref="fileinput" type="file" @change="handleChange">
+          <span v-show="!isSupported" class="vicp-no-supported-hint">{{
+            lang.noSupported
+          }}</span>
+          <input
+            v-show="false"
+            v-if="step == 1"
+            ref="fileinput"
+            type="file"
+            @change="handleChange"
+          >
         </div>
         </div>
         <div v-show="hasError" class="vicp-error">
         <div v-show="hasError" class="vicp-error">
           <i class="vicp-icon2" />
           <i class="vicp-icon2" />
@@ -58,8 +66,14 @@
                 @mouseup="createImg"
                 @mouseup="createImg"
                 @mouseout="createImg"
                 @mouseout="createImg"
               >
               >
-              <div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-1" />
-              <div :style="sourceImgShadeStyle" class="vicp-img-shade vicp-img-shade-2" />
+              <div
+                :style="sourceImgShadeStyle"
+                class="vicp-img-shade vicp-img-shade-1"
+              />
+              <div
+                :style="sourceImgShadeStyle"
+                class="vicp-img-shade vicp-img-shade-2"
+              />
             </div>
             </div>
 
 
             <div class="vicp-range">
             <div class="vicp-range">
@@ -86,8 +100,16 @@
             </div>
             </div>
 
 
             <div v-if="!noRotate" class="vicp-rotate">
             <div v-if="!noRotate" class="vicp-rotate">
-              <i @mousedown="startRotateLeft" @mouseout="endRotate" @mouseup="endRotate">↺</i>
-              <i @mousedown="startRotateRight" @mouseout="endRotate" @mouseup="endRotate">↻</i>
+              <i
+                @mousedown="startRotateLeft"
+                @mouseout="endRotate"
+                @mouseup="endRotate"
+              >↺</i>
+              <i
+                @mousedown="startRotateRight"
+                @mouseout="endRotate"
+                @mouseup="endRotate"
+              >↻</i>
             </div>
             </div>
           </div>
           </div>
           <div v-show="true" class="vicp-crop-right">
           <div v-show="true" class="vicp-crop-right">
@@ -96,7 +118,10 @@
                 <img :src="createImgUrl" :style="previewStyle">
                 <img :src="createImgUrl" :style="previewStyle">
                 <span>{{ lang.preview }}</span>
                 <span>{{ lang.preview }}</span>
               </div>
               </div>
-              <div v-if="!noCircle" class="vicp-preview-item vicp-preview-item-circle">
+              <div
+                v-if="!noCircle"
+                class="vicp-preview-item vicp-preview-item-circle"
+              >
                 <img :src="createImgUrl" :style="previewStyle">
                 <img :src="createImgUrl" :style="previewStyle">
                 <span>{{ lang.preview }}</span>
                 <span>{{ lang.preview }}</span>
               </div>
               </div>
@@ -105,15 +130,25 @@
         </div>
         </div>
         <div class="vicp-operate">
         <div class="vicp-operate">
           <a @click="setStep(1)" @mousedown="ripple">{{ lang.btn.back }}</a>
           <a @click="setStep(1)" @mousedown="ripple">{{ lang.btn.back }}</a>
-          <a class="vicp-operate-btn" @click="prepareUpload" @mousedown="ripple">{{ lang.btn.save }}</a>
+          <a
+            class="vicp-operate-btn"
+            @click="prepareUpload"
+            @mousedown="ripple"
+          >{{ lang.btn.save }}</a>
         </div>
         </div>
       </div>
       </div>
 
 
       <div v-if="step == 3" class="vicp-step3">
       <div v-if="step == 3" class="vicp-step3">
         <div class="vicp-upload">
         <div class="vicp-upload">
-          <span v-show="loading === 1" class="vicp-loading">{{ lang.loading }}</span>
+          <span v-show="loading === 1" class="vicp-loading">{{
+            lang.loading
+          }}</span>
           <div class="vicp-progress-wrap">
           <div class="vicp-progress-wrap">
-            <span v-show="loading === 1" :style="progressStyle" class="vicp-progress" />
+            <span
+              v-show="loading === 1"
+              :style="progressStyle"
+              class="vicp-progress"
+            />
           </div>
           </div>
           <div v-show="hasError" class="vicp-error">
           <div v-show="hasError" class="vicp-error">
             <i class="vicp-icon2" />
             <i class="vicp-icon2" />
@@ -136,7 +171,8 @@
 
 
 <script>
 <script>
 'use strict'
 'use strict'
-import request from '@/utils/request'
+// import request from '@/utils/request'
+import { postfileauth } from '@/boot/axios_request'
 import language from './utils/language.js'
 import language from './utils/language.js'
 import mimes from './utils/mimes.js'
 import mimes from './utils/mimes.js'
 import data2blob from './utils/data2blob.js'
 import data2blob from './utils/data2blob.js'
@@ -479,7 +515,7 @@ export default {
     // 设置图片源
     // 设置图片源
     setSourceImg(file) {
     setSourceImg(file) {
       const fr = new FileReader()
       const fr = new FileReader()
-      fr.onload = e => {
+      fr.onload = (e) => {
         this.sourceImgUrl = fr.result
         this.sourceImgUrl = fr.result
         this.startCrop()
         this.startCrop()
       }
       }
@@ -671,16 +707,8 @@ export default {
     // 缩放原图
     // 缩放原图
     zoomImg(newRange) {
     zoomImg(newRange) {
       const { sourceImgMasking, scale } = this
       const { sourceImgMasking, scale } = this
-      const {
-        maxWidth,
-        maxHeight,
-        minWidth,
-        minHeight,
-        width,
-        height,
-        x,
-        y
-      } = scale
+      const { maxWidth, maxHeight, minWidth, minHeight, width, height, x, y } =
+        scale
       const sim = sourceImgMasking
       const sim = sourceImgMasking
       // 蒙版宽高
       // 蒙版宽高
       const sWidth = sim.width
       const sWidth = sim.width
@@ -758,56 +786,57 @@ export default {
       }
       }
     },
     },
     // 上传图片
     // 上传图片
-    upload() {
-      const {
-        lang,
-        imgFormat,
-        mime,
-        url,
-        params,
-        field,
-        ki,
-        createImgUrl
-      } = this
+    // 修改 upload 方法
+    async upload() {
+      const { imgFormat, mime, url, params, createImgUrl } = this
+
       const fmData = new FormData()
       const fmData = new FormData()
+
+      // 添加裁剪后的图片
       fmData.append(
       fmData.append(
-        field,
+        'avatar',
         data2blob(createImgUrl, mime),
         data2blob(createImgUrl, mime),
-        field + '.' + imgFormat
+        `avatar.${imgFormat}`
       )
       )
+
       // 添加其他参数
       // 添加其他参数
-      if (typeof params === 'object' && params) {
-        Object.keys(params).forEach(k => {
+      if (params) {
+        Object.keys(params).forEach((k) => {
           fmData.append(k, params[k])
           fmData.append(k, params[k])
         })
         })
       }
       }
-      // 监听进度回调
-      // const uploadProgress = (event) => {
-      //   if (event.lengthComputable) {
-      //     this.progress = 100 * Math.round(event.loaded) / event.total
-      //   }
-      // }
-      // 上传文件
+
       this.reset()
       this.reset()
       this.loading = 1
       this.loading = 1
       this.setStep(3)
       this.setStep(3)
-      request({
-        url,
-        method: 'post',
-        data: fmData
-      })
-        .then(resData => {
-          this.loading = 2
-          this.$emit('crop-upload-success', resData.data)
-        })
-        .catch(err => {
-          if (this.value) {
-            this.loading = 3
-            this.hasError = true
-            this.errorMsg = lang.fail
-            this.$emit('crop-upload-fail', err, field, ki)
+
+      try {
+        console.log('url', url)
+        console.log('fmData', fmData)
+
+        const response = await postfileauth(url, fmData, {
+          headers: {
+            'Content-Type': 'multipart/form-data'
           }
           }
         })
         })
+
+        console.log('response', response)
+        this.loading = 2
+
+        // 确保使用后端返回的头像URL
+        if (response && response.data && response.data.avatar) {
+          this.image = response.data.avatar
+          this.$store.commit('user/SET_AVATAR', response.data.avatar)
+        }
+
+        this.$emit('crop-upload-success', response)
+      } catch (err) {
+        console.error('上传错误:', err)
+        this.loading = 3
+        this.hasError = true
+        this.errorMsg = this.lang.fail
+        this.$emit('crop-upload-fail', err)
+      }
     },
     },
     closeHandler(e) {
     closeHandler(e) {
       if (this.value && (e.key === 'Escape' || e.keyCode === 27)) {
       if (this.value && (e.key === 'Escape' || e.keyCode === 27)) {

+ 118 - 0
templates/src/components/date/index.js

@@ -0,0 +1,118 @@
+/**
+ * 日期格式化工具
+ * @param {string|Date} date - 日期字符串或Date对象
+ * @param {string} format - 输出格式,支持以下占位符:
+ *   YYYY - 四位年份
+ *   YY - 两位年份
+ *   MM - 两位月份
+ *   DD - 两位日期
+ *   HH - 24小时制小时
+ *   mm - 分钟
+ *   ss - 秒钟
+ *   SSS - 毫秒
+ * @returns {string} 格式化后的日期字符串
+ */
+export function formatDate(date, format = 'YYYY-MM-DD HH:mm') {
+  // 处理空值
+  if (!date) return ''
+
+  // 创建Date对象
+  const dateObj = date instanceof Date ? date : new Date(date)
+
+  // 处理无效日期
+  if (isNaN(dateObj.getTime())) {
+    console.warn('formatDate: Invalid date input, returning empty string')
+    return ''
+  }
+
+  // 填充数字到指定长度
+  const pad = (num, length = 2) => String(num).padStart(length, '0')
+
+  // 提取日期组成部分
+  const year = dateObj.getFullYear()
+  const month = dateObj.getMonth() + 1
+  const day = dateObj.getDate()
+  const hours = dateObj.getHours()
+  const minutes = dateObj.getMinutes()
+  const seconds = dateObj.getSeconds()
+  const milliseconds = dateObj.getMilliseconds()
+
+  // 替换格式占位符
+  return format
+    .replace(/YYYY/g, pad(year, 4))
+    .replace(/YY/g, pad(year % 100, 2))
+    .replace(/MM/g, pad(month))
+    .replace(/DD/g, pad(day))
+    .replace(/HH/g, pad(hours))
+    .replace(/mm/g, pad(minutes))
+    .replace(/ss/g, pad(seconds))
+    .replace(/SSS/g, pad(milliseconds, 3))
+}
+
+/**
+ * 相对时间格式转换
+ * @param {string|Date} date - 日期字符串或Date对象
+ * @returns {string} 相对于当前时间的描述(如"3天前")
+ */
+export function relativeTime(date) {
+  // 处理空值
+  if (!date) return ''
+
+  // 创建Date对象
+  const dateObj = date instanceof Date ? date : new Date(date)
+
+  // 处理无效日期
+  if (isNaN(dateObj.getTime())) {
+    console.warn('relativeTime: Invalid date input, returning empty string')
+    return ''
+  }
+
+  const now = new Date()
+  const diffInSeconds = Math.floor((now - dateObj) / 1000)
+
+  const intervals = {
+    年: 31536000,
+    月: 2592000,
+    天: 86400,
+    小时: 3600,
+    分钟: 60
+  }
+
+  for (const [unit, seconds] of Object.entries(intervals)) {
+    const interval = Math.floor(diffInSeconds / seconds)
+    if (interval >= 1) {
+      return `${interval}${unit}前`
+    }
+  }
+
+  return diffInSeconds < 10 ? '刚刚' : `${diffInSeconds}秒前`
+}
+
+/**
+ * 时长格式化(毫秒转可读格式)
+ * @param {number} milliseconds - 毫秒数
+ * @returns {string} 格式化后的时长(如"01:23:45")
+ */
+export function formatDuration(milliseconds) {
+  if (!milliseconds && milliseconds !== 0) return ''
+
+  const totalSeconds = Math.floor(milliseconds / 1000)
+  const hours = Math.floor(totalSeconds / 3600)
+  const minutes = Math.floor((totalSeconds % 3600) / 60)
+  const seconds = totalSeconds % 60
+
+  return [hours ? pad(hours) : null, pad(minutes), pad(seconds)]
+    .filter(Boolean)
+    .join(':')
+}
+
+// 内部使用的填充函数
+function pad(num) {
+  return num.toString().padStart(2, '0')
+}
+
+// 示例使用
+// console.log(formatDate('2025-08-07T14:33:37.043403')); // 默认格式
+// console.log(formatDate('2025-08-07T14:33:37.043403', 'YYYY年MM月DD日 HH:mm:ss')); // 自定义格式
+// console.log(relativeTime(new Date(Date.now() - 30000))); // 30秒前
+// console.log(formatDuration(12345678)); // 03:25:45

+ 3 - 0
templates/src/store/getters.js

@@ -10,6 +10,9 @@ const getters = {
   name: state => state.user.name,
   name: state => state.user.name,
   introduction: state => state.user.introduction,
   introduction: state => state.user.introduction,
   roles: state => state.user.roles,
   roles: state => state.user.roles,
+  email: state => state.user.email,
+  phone: state => state.user.phone,
+  address: state => state.user.address,
   permission_routes: state => state.permission.routes,
   permission_routes: state => state.permission.routes,
   errorLogs: state => state.errorLog.logs
   errorLogs: state => state.errorLog.logs
 }
 }

+ 22 - 4
templates/src/store/modules/user.js

@@ -10,7 +10,10 @@ const state = {
   name: '',
   name: '',
   avatar: '',
   avatar: '',
   introduction: '',
   introduction: '',
-  roles: []
+  roles: [],
+  email: '',
+  phone: '',
+  address: ''
 }
 }
 
 
 const mutations = {
 const mutations = {
@@ -28,6 +31,15 @@ const mutations = {
   },
   },
   SET_ROLES: (state, roles) => {
   SET_ROLES: (state, roles) => {
     state.roles = roles
     state.roles = roles
+  },
+  SET_EMAIL: (state, email) => {
+    state.email = email
+  },
+  SET_PHONE: (state, phone) => {
+    state.phone = phone
+  },
+  SET_ADDRESS: (state, address) => {
+    state.address = address
   }
   }
 }
 }
 
 
@@ -82,7 +94,7 @@ const actions = {
             reject('Verification failed, please Login again.')
             reject('Verification failed, please Login again.')
           }
           }
 
 
-          const { roles, name, avatar, introduction } = data
+          const { roles, name, avatar_url, introduction, email, phone, address } = data
 
 
           if (!roles || roles.length <= 0) {
           if (!roles || roles.length <= 0) {
             reject('getInfo: roles must be a non-null array!')
             reject('getInfo: roles must be a non-null array!')
@@ -90,12 +102,18 @@ const actions = {
 
 
           commit('SET_ROLES', roles)
           commit('SET_ROLES', roles)
           commit('SET_NAME', name)
           commit('SET_NAME', name)
-          commit('SET_AVATAR', avatar)
+          commit('SET_AVATAR', avatar_url)
           commit('SET_INTRODUCTION', introduction)
           commit('SET_INTRODUCTION', introduction)
+          commit('SET_EMAIL', email)
+          commit('SET_PHONE', phone)
+          commit('SET_ADDRESS', address)
           console.log('[user.js]当前的roles', roles)
           console.log('[user.js]当前的roles', roles)
           console.log('[user.js]当前的name', name)
           console.log('[user.js]当前的name', name)
-          console.log('[user.js]当前的avatar', avatar)
+          console.log('[user.js]当前的avatar', avatar_url)
           console.log('[user.js]当前的introduction', introduction)
           console.log('[user.js]当前的introduction', introduction)
+          console.log('[user.js]当前的email', email)
+          console.log('[user.js]当前的phone', phone)
+          console.log('[user.js]当前的address', address)
           resolve(data)
           resolve(data)
         })
         })
         .catch((error) => {
         .catch((error) => {

+ 6 - 2
templates/src/views/components-demo/avatar-upload.vue

@@ -17,7 +17,7 @@
       :width="300"
       :width="300"
       :height="300"
       :height="300"
       url="https://httpbin.org/post"
       url="https://httpbin.org/post"
-      lang-type="en"
+      lang-type="zh"
       @close="close"
       @close="close"
       @crop-upload-success="cropSuccess"
       @crop-upload-success="cropSuccess"
     />
     />
@@ -27,6 +27,7 @@
 <script>
 <script>
 import ImageCropper from '@/components/ImageCropper'
 import ImageCropper from '@/components/ImageCropper'
 import PanThumb from '@/components/PanThumb'
 import PanThumb from '@/components/PanThumb'
+import { mapGetters } from 'vuex'
 
 
 export default {
 export default {
   name: 'AvatarUploadDemo',
   name: 'AvatarUploadDemo',
@@ -35,9 +36,12 @@ export default {
     return {
     return {
       imagecropperShow: false,
       imagecropperShow: false,
       imagecropperKey: 0,
       imagecropperKey: 0,
-      image: 'https://wpimg.wallstcn.com/577965b9-bb9e-4e02-9f0c-095b41417191'
+      image: this.$store.getters.avatar
     }
     }
   },
   },
+  computed: {
+    ...mapGetters(['avatar']) // 从 Vuex 获取
+  },
   methods: {
   methods: {
     cropSuccess(resData) {
     cropSuccess(resData) {
       this.imagecropperShow = false
       this.imagecropperShow = false

+ 4 - 4
templates/src/views/login/index.vue

@@ -201,9 +201,9 @@ export default {
         username: [
         username: [
           { required: true, message: '请输入用户名', trigger: 'blur' },
           { required: true, message: '请输入用户名', trigger: 'blur' },
           {
           {
-            min: 3,
+            min: 1,
             max: 15,
             max: 15,
-            message: '用户名长度在3到15个字符之间',
+            message: '用户名长度在1到15个字符之间',
             trigger: 'blur'
             trigger: 'blur'
           }
           }
         ],
         ],
@@ -225,9 +225,9 @@ export default {
         username: [
         username: [
           { required: true, message: '请输入用户名', trigger: 'blur' },
           { required: true, message: '请输入用户名', trigger: 'blur' },
           {
           {
-            min: 3,
+            min: 1,
             max: 15,
             max: 15,
-            message: '用户名长度在3到15个字符之间',
+            message: '用户名长度在1到15个字符之间',
             trigger: 'blur'
             trigger: 'blur'
           }
           }
         ],
         ],

+ 412 - 13
templates/src/views/profile/components/Account.vue

@@ -1,38 +1,437 @@
 <template>
 <template>
-  <el-form>
-    <el-form-item label="Name">
-      <el-input v-model.trim="user.name" />
-    </el-form-item>
-    <el-form-item label="Email">
-      <el-input v-model.trim="user.email" />
-    </el-form-item>
-    <el-form-item>
-      <el-button type="primary" @click="submit">Update</el-button>
-    </el-form-item>
-  </el-form>
+  <div>
+    <el-form>
+      <el-row :gutter="20">
+        <!-- 第一列:基本资料 -->
+        <el-col :span="8">
+          <el-card class="form-section">
+            <div slot="header" class="clearfix">
+              <span>基本资料</span>
+            </div>
+            <div class="box-center">
+              <div class="avatar-container box-center">
+                <el-button
+                  type="primary"
+                  icon="el-icon-upload"
+                  class="change-avatar-btn"
+                  @click="imagecropperShow = true"
+                >
+                  更换头像
+                </el-button>
+              </div>
+            </div>
+
+            <el-form-item label="姓名">
+              <el-input v-model.trim="user.name" />
+            </el-form-item>
+
+            <el-form-item label="邮箱">
+              <el-input v-model.trim="user.email" />
+            </el-form-item>
+
+            <el-form-item label="手机号">
+              <el-input v-model.trim="user.phone" />
+            </el-form-item>
+
+            <el-form-item label="地址">
+              <el-input v-model.trim="user.address" type="textarea" :rows="2" />
+            </el-form-item>
+          </el-card>
+        </el-col>
+
+        <!-- 第二列:学术信息 -->
+
+        <el-col :span="8">
+          <el-card class="form-section">
+            <div slot="header" class="clearfix">
+              <span>学术信息</span>
+            </div>
+
+            <el-form-item label="学术身份">
+              <el-select
+                v-model="user.academic_profile.role"
+                placeholder="请选择身份"
+              >
+                <el-option
+                  v-for="role in academicRoles"
+                  :key="role.value"
+                  :label="role.label"
+                  :value="role.value"
+                />
+              </el-select>
+            </el-form-item>
+
+            <el-form-item label="入学年份">
+              <el-input
+                v-model.number="user.academic_profile.enrollment_year"
+                type="number"
+              />
+            </el-form-item>
+
+            <el-form-item label="毕业年份">
+              <el-input
+                v-model.number="user.academic_profile.graduation_year"
+                type="number"
+              />
+            </el-form-item>
+
+            <el-form-item label="院系/研究所">
+              <el-input v-model.trim="user.academic_profile.department" />
+            </el-form-item>
+
+            <el-form-item label="专业方向">
+              <el-input v-model.trim="user.academic_profile.major" />
+            </el-form-item>
+
+            <el-form-item label="研究方向">
+              <el-input
+                v-model.trim="user.academic_profile.research_tags"
+                placeholder="多个方向用逗号分隔 (如:人工智能,教育技术)"
+              />
+            </el-form-item>
+
+            <el-form-item label="技能标签">
+              <el-input
+                v-model.trim="user.academic_profile.skill_tags"
+                placeholder="多个技能用逗号分隔 (如:Python,数据分析)"
+              />
+            </el-form-item>
+          </el-card>
+        </el-col>
+
+        <!-- 第三列:小组信息 -->
+
+        <el-col :span="8">
+          <el-card class="form-section">
+            <div slot="header" class="clearfix">
+              <span>小组信息</span>
+              <el-button
+                style="float: right; padding: 3px 0"
+                type="text"
+                @click="addGroupDialogVisible = true"
+              >
+                加入小组
+              </el-button>
+            </div>
+
+            <div v-if="user.group_memberships && user.group_memberships.length">
+              <div
+                v-for="(membership, index) in user.group_memberships"
+                :key="index"
+                class="group-item"
+              >
+                <div class="group-header">
+                  <span class="group-name">{{ membership.group.name }}</span>
+                  <el-tag
+                    size="mini"
+                    :type="getRoleTagType(membership.role)"
+                    style="margin-left: 10px"
+                  >
+                    {{ getRoleLabel(membership.role) }}
+                  </el-tag>
+                  <el-tag
+                    size="mini"
+                    :type="getStatusTagType(membership.status)"
+                    style="margin-left: 5px"
+                  >
+                    {{ getStatusLabel(membership.status) }}
+                  </el-tag>
+                </div>
+
+                <div class="group-info">
+                  <div>
+                    <i class="el-icon-time" /> 加入时间:
+                    {{ formatDate(membership.joined_at) }}
+                  </div>
+                  <div v-if="membership.left_at">
+                    <i class="el-icon-switch-button" /> 离开时间:
+                    {{ formatDate(membership.left_at) }}
+                  </div>
+                </div>
+
+                <div class="group-actions">
+                  <el-button
+                    v-if="membership.status === 'ACT'"
+                    type="danger"
+                    size="mini"
+                    @click="leaveGroup(membership)"
+                  >
+                    退出小组
+                  </el-button>
+                  <el-button
+                    v-if="membership.status !== 'ACT'"
+                    type="primary"
+                    size="mini"
+                    @click="rejoinGroup(membership)"
+                  >
+                    重新加入
+                  </el-button>
+                </div>
+              </div>
+            </div>
+
+            <div v-else class="empty-groups">
+              <i class="el-icon-info" /> 您目前没有加入任何小组
+            </div>
+          </el-card>
+        </el-col>
+      </el-row>
+
+      <el-form-item>
+        <el-button type="primary" @click="submit">更新全部信息</el-button>
+        <el-button @click="cancel">取消</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 加入小组对话框 -->
+    <el-dialog
+      title="加入小组"
+      :visible.sync="addGroupDialogVisible"
+      width="30%"
+    >
+      <el-form>
+        <el-form-item label="选择小组">
+          <el-select v-model="selectedGroupId" placeholder="请选择小组">
+            <el-option
+              v-for="group in availableGroups"
+              :key="group.group_id"
+              :label="group.name"
+              :value="group.group_id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="addGroupDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="joinGroup">加入</el-button>
+      </span>
+    </el-dialog>
+    <image-cropper
+      v-show="imagecropperShow"
+      :key="imagecropperKey"
+      :width="300"
+      :height="300"
+      url="/user/avatar-upload/"
+      lang-type="zh"
+      @close="close"
+      @crop-upload-success="cropSuccess"
+    />
+  </div>
 </template>
 </template>
 
 
 <script>
 <script>
+import ImageCropper from '@/components/ImageCropper'
+import { formatDate } from '@/components/date'
+
+import { postauth } from '@/boot/axios_request'
+
 export default {
 export default {
+  components: { ImageCropper },
   props: {
   props: {
     user: {
     user: {
       type: Object,
       type: Object,
       default: () => {
       default: () => {
         return {
         return {
           name: '',
           name: '',
-          email: ''
+          email: '',
+          phone: '',
+          address: '',
+          avatar: '',
+          academic_profile: {
+            role: '',
+            enrollment_year: '',
+            graduation_year: '',
+            department: '',
+            major: '',
+            research_tags: '',
+            skill_tags: ''
+          },
+          group_memberships: []
         }
         }
       }
       }
+    },
+    availableGroups: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data() {
+    return {
+      profilepath: 'user/profile/',
+      imagecropperShow: false,
+      imagecropperKey: 0,
+      image: this.$store.getters.avatar,
+      selectedGroupId: null,
+      addGroupDialogVisible: false,
+      academicRoles: [
+        { value: 'UG', label: '本科生' },
+        { value: 'MS', label: '硕士生' },
+        { value: 'PhD', label: '博士生' },
+        { value: 'PD', label: '博士后' },
+        { value: 'FAC', label: '教职工' },
+        { value: 'RES', label: '研究员' }
+      ],
+      memberRoles: {
+        LEAD: { label: '组长', type: 'danger' },
+        DEP: { label: '副组长', type: 'warning' },
+        CORE: { label: '核心成员', type: 'success' },
+        MEM: { label: '普通成员', type: '' },
+        ADV: { label: '指导老师', type: 'info' },
+        OBS: { label: '观察员', type: 'info' }
+      },
+      memberStatus: {
+        ACT: { label: '活跃', type: 'success' },
+        LV: { label: '请假', type: 'warning' },
+        INAC: { label: '不活跃', type: 'danger' },
+        GRAD: { label: '已毕业', type: 'info' },
+        TRF: { label: '已转组', type: 'info' }
+      }
     }
     }
   },
   },
   methods: {
   methods: {
+    formatDate,
     submit() {
     submit() {
+      postauth(this.profilepath, this.user).then((res) => {
+        this.handleSuccess()
+      })
+    },
+    handleSuccess() {
+      this.$store.dispatch('user/getInfo')
       this.$message({
       this.$message({
-        message: 'User information has been updated successfully',
+        message: '用户信息已成功更新',
         type: 'success',
         type: 'success',
         duration: 5 * 1000
         duration: 5 * 1000
       })
       })
+    },
+    cancel() {
+      this.$emit('cancel')
+    },
+    cropSuccess(resData) {
+      this.imagecropperShow = false
+      this.imagecropperKey += 1
+      console.log(resData)
+      this.$store.dispatch('user/getInfo')
+    },
+    close() {
+      this.imagecropperShow = false
+    },
+    getRoleLabel(role) {
+      return this.memberRoles[role]?.label || role
+    },
+    getStatusLabel(status) {
+      return this.memberStatus[status]?.label || status
+    },
+    getRoleTagType(role) {
+      return this.memberRoles[role]?.type || ''
+    },
+    getStatusTagType(status) {
+      return this.memberStatus[status]?.type || ''
+    },
+    joinGroup() {
+      if (!this.selectedGroupId) {
+        this.$message.warning('请选择要加入的小组')
+        return
+      }
+
+      // 这里调用加入小组的API
+      this.$message.success('加入小组请求已发送')
+      this.addGroupDialogVisible = false
+    },
+    leaveGroup(membership) {
+      this.$confirm('确定要退出该小组吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        // 这里调用退出小组的API
+        membership.status = 'TRF'
+        membership.left_at = new Date()
+        this.$message.success('已成功退出小组')
+      })
+    },
+    rejoinGroup(membership) {
+      this.$confirm('确定要重新加入该小组吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'info'
+      }).then(() => {
+        // 这里调用重新加入小组的API
+        membership.status = 'ACT'
+        membership.left_at = null
+        this.$message.success('已重新加入小组')
+      })
     }
     }
   }
   }
 }
 }
 </script>
 </script>
+
+<style lang="scss" scoped>
+.form-section {
+  margin-bottom: 20px;
+}
+
+.avatar-container {
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  position: relative;
+  width: 150px;
+  margin-bottom: 50px;
+}
+
+.change-avatar-btn {
+  position: absolute;
+  bottom: -35px;
+  left: 50%;
+  transform: translateX(-50%);
+  z-index: 2;
+}
+
+.box-center {
+  left: 20% auto;
+  display: table;
+}
+
+.group-item {
+  padding: 15px;
+  margin-bottom: 15px;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  background-color: #f8f9fa;
+
+  .group-header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 10px;
+
+    .group-name {
+      font-weight: bold;
+    }
+  }
+
+  .group-info {
+    color: #606266;
+    font-size: 13px;
+    margin-bottom: 10px;
+
+    div {
+      margin-bottom: 5px;
+    }
+  }
+
+  .group-actions {
+    text-align: right;
+  }
+}
+
+.empty-groups {
+  text-align: center;
+  color: #909399;
+  padding: 20px;
+  font-size: 14px;
+
+  i {
+    margin-right: 5px;
+  }
+}
+</style>

+ 15 - 12
templates/src/views/profile/components/UserCard.vue

@@ -1,25 +1,25 @@
 <template>
 <template>
   <el-card style="margin-bottom:20px;">
   <el-card style="margin-bottom:20px;">
     <div slot="header" class="clearfix">
     <div slot="header" class="clearfix">
-      <span>About me</span>
+      <span>个人资料</span>
     </div>
     </div>
 
 
     <div class="user-profile">
     <div class="user-profile">
       <div class="box-center">
       <div class="box-center">
-        <pan-thumb :image="user.avatar" :height="'100px'" :width="'100px'" :hoverable="false">
-          <div>Hello</div>
-          {{ user.role }}
+        <pan-thumb :image="avatar" :height="'100px'" :width="'100px'" :hoverable="false">
+          <div>你好</div>
+          {{ roles }}
         </pan-thumb>
         </pan-thumb>
       </div>
       </div>
       <div class="box-center">
       <div class="box-center">
-        <div class="user-name text-center">{{ user.name }}</div>
-        <div class="user-role text-center text-muted">{{ user.role | uppercaseFirst }}</div>
+        <div class="user-name text-center">{{ name }}</div>
+        <div class="user-role text-center text-muted">{{ roles }}</div>
       </div>
       </div>
     </div>
     </div>
 
 
     <div class="user-bio">
     <div class="user-bio">
       <div class="user-education user-bio-section">
       <div class="user-education user-bio-section">
-        <div class="user-bio-section-header"><svg-icon icon-class="education" /><span>Education</span></div>
+        <div class="user-bio-section-header"><svg-icon icon-class="education" /><span>教育背景</span></div>
         <div class="user-bio-section-body">
         <div class="user-bio-section-body">
           <div class="text-muted">
           <div class="text-muted">
             JS in Computer Science from the University of Technology
             JS in Computer Science from the University of Technology
@@ -28,10 +28,10 @@
       </div>
       </div>
 
 
       <div class="user-skills user-bio-section">
       <div class="user-skills user-bio-section">
-        <div class="user-bio-section-header"><svg-icon icon-class="skill" /><span>Skills</span></div>
+        <div class="user-bio-section-header"><svg-icon icon-class="skill" /><span>技能</span></div>
         <div class="user-bio-section-body">
         <div class="user-bio-section-body">
           <div class="progress-item">
           <div class="progress-item">
-            <span>Vue</span>
+            <span>研究</span>
             <el-progress :percentage="70" />
             <el-progress :percentage="70" />
           </div>
           </div>
           <div class="progress-item">
           <div class="progress-item">
@@ -54,6 +54,7 @@
 
 
 <script>
 <script>
 import PanThumb from '@/components/PanThumb'
 import PanThumb from '@/components/PanThumb'
+import { mapGetters } from 'vuex'
 
 
 export default {
 export default {
   components: { PanThumb },
   components: { PanThumb },
@@ -62,14 +63,16 @@ export default {
       type: Object,
       type: Object,
       default: () => {
       default: () => {
         return {
         return {
-          name: '',
-          email: '',
-          avatar: '',
+
           role: ''
           role: ''
         }
         }
       }
       }
     }
     }
+  },
+  computed: {
+    ...mapGetters(['name', 'avatar', 'roles'])
   }
   }
+
 }
 }
 </script>
 </script>
 
 

+ 19 - 14
templates/src/views/profile/index.vue

@@ -2,27 +2,24 @@
   <div class="app-container">
   <div class="app-container">
     <div v-if="user">
     <div v-if="user">
       <el-row :gutter="20">
       <el-row :gutter="20">
-
         <el-col :span="6" :xs="24">
         <el-col :span="6" :xs="24">
           <user-card :user="user" />
           <user-card :user="user" />
         </el-col>
         </el-col>
-
         <el-col :span="18" :xs="24">
         <el-col :span="18" :xs="24">
           <el-card>
           <el-card>
             <el-tabs v-model="activeTab">
             <el-tabs v-model="activeTab">
+              <el-tab-pane label="用户信息" name="account">
+                <account :user="user" />
+              </el-tab-pane>
               <el-tab-pane label="Activity" name="activity">
               <el-tab-pane label="Activity" name="activity">
                 <activity />
                 <activity />
               </el-tab-pane>
               </el-tab-pane>
               <el-tab-pane label="Timeline" name="timeline">
               <el-tab-pane label="Timeline" name="timeline">
                 <timeline />
                 <timeline />
               </el-tab-pane>
               </el-tab-pane>
-              <el-tab-pane label="Account" name="account">
-                <account :user="user" />
-              </el-tab-pane>
             </el-tabs>
             </el-tabs>
           </el-card>
           </el-card>
         </el-col>
         </el-col>
-
       </el-row>
       </el-row>
     </div>
     </div>
   </div>
   </div>
@@ -41,15 +38,11 @@ export default {
   data() {
   data() {
     return {
     return {
       user: {},
       user: {},
-      activeTab: 'activity'
+      activeTab: 'account'
     }
     }
   },
   },
   computed: {
   computed: {
-    ...mapGetters([
-      'name',
-      'avatar',
-      'roles'
-    ])
+    ...mapGetters(['name', 'avatar', 'roles', 'email', 'phone', 'address'])
   },
   },
   created() {
   created() {
     this.getUser()
     this.getUser()
@@ -59,8 +52,20 @@ export default {
       this.user = {
       this.user = {
         name: this.name,
         name: this.name,
         role: this.roles.join(' | '),
         role: this.roles.join(' | '),
-        email: 'admin@test.com',
-        avatar: this.avatar
+        email: this.email,
+        phone: this.phone,
+        address: this.address,
+        avatar: this.avatar,
+        academic_profile: {
+          role: '',
+          enrollment_year: '',
+          graduation_year: '',
+          department: '电子科技大学-机械与电气工程学院-机器人感知与信息融合小组',
+          major: '',
+          research_tags: '',
+          skill_tags: ''
+        },
+        group_memberships: []
       }
       }
     }
     }
   }
   }

+ 3 - 1
userprofile/filter.py

@@ -14,7 +14,9 @@ class UsersFilter(FilterSet):
             'developer': ['exact'],
             'developer': ['exact'],
             't_code': ['icontains'],
             't_code': ['icontains'],
             'ip': ['icontains'],
             'ip': ['icontains'],
-            'avatar': ['icontains'],
+            'email': ['icontains', 'exact'],
+            'phone': ['icontains', 'exact'],
+            'address': ['icontains', 'exact'],
             'create_time': ['exact', 'lt', 'gt'],
             'create_time': ['exact', 'lt', 'gt'],
             'update_time': ['exact', 'lt', 'gt'],
             'update_time': ['exact', 'lt', 'gt'],
         }
         }

+ 13 - 6
userprofile/migrations/0001_initial.py

@@ -1,5 +1,6 @@
-# Generated by Django 4.1.2 on 2025-08-05 14:01
+# Generated by Django 4.1.2 on 2025-08-07 22:15
 
 
+from django.conf import settings
 from django.db import migrations, models
 from django.db import migrations, models
 import django.db.models.deletion
 import django.db.models.deletion
 
 
@@ -9,6 +10,7 @@ class Migration(migrations.Migration):
     initial = True
     initial = True
 
 
     dependencies = [
     dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
     ]
     ]
 
 
     operations = [
     operations = [
@@ -19,15 +21,20 @@ class Migration(migrations.Migration):
                 ('name', models.CharField(max_length=80, verbose_name='姓名')),
                 ('name', models.CharField(max_length=80, verbose_name='姓名')),
                 ('openid', models.CharField(max_length=100, unique=True, verbose_name='OPENID')),
                 ('openid', models.CharField(max_length=100, unique=True, verbose_name='OPENID')),
                 ('appid', models.CharField(max_length=100, verbose_name='APPID')),
                 ('appid', models.CharField(max_length=100, verbose_name='APPID')),
+                ('roles', models.CharField(choices=[('ADM', '管理员'), ('USR', '普通用户'), ('DEV', '开发者')], default='USR', max_length=10, verbose_name='角色')),
                 ('vip', models.PositiveIntegerField(default=1, verbose_name='VIP等级')),
                 ('vip', models.PositiveIntegerField(default=1, verbose_name='VIP等级')),
                 ('vip_time', models.DateTimeField(auto_now_add=True, verbose_name='VIP生效时间')),
                 ('vip_time', models.DateTimeField(auto_now_add=True, verbose_name='VIP生效时间')),
                 ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')),
                 ('is_delete', models.BooleanField(default=False, verbose_name='删除标记')),
                 ('developer', models.BooleanField(default=False, verbose_name='开发者标记')),
                 ('developer', models.BooleanField(default=False, verbose_name='开发者标记')),
                 ('t_code', models.CharField(max_length=100, unique=True, verbose_name='交易验证码')),
                 ('t_code', models.CharField(max_length=100, unique=True, verbose_name='交易验证码')),
                 ('ip', models.GenericIPAddressField(verbose_name='注册IP')),
                 ('ip', models.GenericIPAddressField(verbose_name='注册IP')),
-                ('avatar', models.CharField(default='/static/img/user.jpg', max_length=200, verbose_name='用户头像')),
+                ('avatar', models.ImageField(blank=True, default='/static/img/user.jpg', null=True, upload_to='avatars/', verbose_name='头像')),
+                ('email', models.EmailField(blank=True, default='email@email.com', max_length=254, null=True, verbose_name='邮箱')),
+                ('phone', models.CharField(blank=True, max_length=20, null=True, verbose_name='手机号')),
+                ('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='地址')),
                 ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
                 ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
                 ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
                 ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
+                ('user_name', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_profile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户')),
             ],
             ],
             options={
             options={
                 'verbose_name': '用户档案',
                 'verbose_name': '用户档案',
@@ -39,7 +46,7 @@ class Migration(migrations.Migration):
         migrations.CreateModel(
         migrations.CreateModel(
             name='AcademicProfile',
             name='AcademicProfile',
             fields=[
             fields=[
-                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='academic_profile', serialize=False, to='userprofile.users', verbose_name='关联用户')),
+                ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='user_academic_profile', serialize=False, to='userprofile.users', verbose_name='关联用户')),
                 ('role', models.CharField(choices=[('UG', '本科生'), ('MS', '硕士生'), ('PhD', '博士生'), ('PD', '博士后'), ('FAC', '教职工'), ('RES', '研究员')], default='MS', max_length=10, verbose_name='学术身份')),
                 ('role', models.CharField(choices=[('UG', '本科生'), ('MS', '硕士生'), ('PhD', '博士生'), ('PD', '博士后'), ('FAC', '教职工'), ('RES', '研究员')], default='MS', max_length=10, verbose_name='学术身份')),
                 ('enrollment_year', models.PositiveIntegerField(help_text='格式:YYYY(如2023)', verbose_name='入学年份')),
                 ('enrollment_year', models.PositiveIntegerField(help_text='格式:YYYY(如2023)', verbose_name='入学年份')),
                 ('graduation_year', models.PositiveIntegerField(blank=True, null=True, verbose_name='预计毕业年份')),
                 ('graduation_year', models.PositiveIntegerField(blank=True, null=True, verbose_name='预计毕业年份')),
@@ -51,7 +58,7 @@ class Migration(migrations.Migration):
             options={
             options={
                 'verbose_name': '学术档案',
                 'verbose_name': '学术档案',
                 'verbose_name_plural': '学术档案',
                 'verbose_name_plural': '学术档案',
-                'db_table': 'academic_profile',
+                'db_table': 'user_academic_profile',
             },
             },
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
@@ -67,7 +74,7 @@ class Migration(migrations.Migration):
             options={
             options={
                 'verbose_name': '教研小组',
                 'verbose_name': '教研小组',
                 'verbose_name_plural': '教研小组',
                 'verbose_name_plural': '教研小组',
-                'db_table': 'research_group',
+                'db_table': 'user_research_group',
                 'ordering': ['-created_at'],
                 'ordering': ['-created_at'],
             },
             },
         ),
         ),
@@ -86,7 +93,7 @@ class Migration(migrations.Migration):
             options={
             options={
                 'verbose_name': '小组成员',
                 'verbose_name': '小组成员',
                 'verbose_name_plural': '小组成员',
                 'verbose_name_plural': '小组成员',
-                'db_table': 'group_membership',
+                'db_table': 'user_group_membership',
                 'ordering': ['-joined_at'],
                 'ordering': ['-joined_at'],
                 'unique_together': {('user', 'group')},
                 'unique_together': {('user', 'group')},
             },
             },

+ 0 - 27
userprofile/migrations/0002_alter_academicprofile_user_and_more.py

@@ -1,27 +0,0 @@
-# Generated by Django 4.1.2 on 2025-08-05 16:06
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('userprofile', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.AlterField(
-            model_name='academicprofile',
-            name='user',
-            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='user_academic_profile', serialize=False, to='userprofile.users', verbose_name='关联用户'),
-        ),
-        migrations.AlterModelTable(
-            name='groupmembership',
-            table='user_group_membership',
-        ),
-        migrations.AlterModelTable(
-            name='researchgroup',
-            table='user_research_group',
-        ),
-    ]

+ 0 - 17
userprofile/migrations/0003_alter_academicprofile_table.py

@@ -1,17 +0,0 @@
-# Generated by Django 4.1.2 on 2025-08-05 16:08
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('userprofile', '0002_alter_academicprofile_user_and_more'),
-    ]
-
-    operations = [
-        migrations.AlterModelTable(
-            name='academicprofile',
-            table='user_academic_profile',
-        ),
-    ]

+ 34 - 2
userprofile/models.py

@@ -1,14 +1,34 @@
 from django.db import models
 from django.db import models
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 from django.utils import timezone
 from django.utils import timezone
+from django.contrib.auth.models import User
 
 
 class Users(models.Model):
 class Users(models.Model):
     """核心用户模型 (继承现有结构)"""
     """核心用户模型 (继承现有结构)"""
+    class UserRole(models.TextChoices):
+        ADMIN = 'ADM', _('管理员')
+        USER = 'USR', _('普通用户')
+        DEVELOPER = 'DEV', _('开发者')
+
+    user_name = models.OneToOneField(
+        User,
+        on_delete=models.CASCADE,
+        primary_key=False,
+        related_name='user_profile',
+        verbose_name='关联用户'
+    )
     user_id = models.AutoField(primary_key=True, verbose_name="用户ID")
     user_id = models.AutoField(primary_key=True, verbose_name="用户ID")
     name = models.CharField(max_length=80, verbose_name='姓名')
     name = models.CharField(max_length=80, verbose_name='姓名')
     openid = models.CharField(max_length=100, unique=True, verbose_name='OPENID')
     openid = models.CharField(max_length=100, unique=True, verbose_name='OPENID')
     appid = models.CharField(max_length=100, verbose_name='APPID')
     appid = models.CharField(max_length=100, verbose_name='APPID')
-    
+
+    roles = models.CharField(    
+
+        max_length=10,
+        choices=UserRole.choices,
+        default=UserRole.USER,
+        verbose_name='角色'
+    )
     # 权限系统
     # 权限系统
     vip = models.PositiveIntegerField(default=1, verbose_name='VIP等级')
     vip = models.PositiveIntegerField(default=1, verbose_name='VIP等级')
     vip_time = models.DateTimeField(auto_now_add=True, verbose_name='VIP生效时间')
     vip_time = models.DateTimeField(auto_now_add=True, verbose_name='VIP生效时间')
@@ -22,7 +42,12 @@ class Users(models.Model):
     ip = models.GenericIPAddressField(verbose_name='注册IP')
     ip = models.GenericIPAddressField(verbose_name='注册IP')
     
     
     # 基本信息
     # 基本信息
-    avatar = models.CharField(max_length=200, default='/static/img/user.jpg', verbose_name='用户头像')
+    avatar = models.ImageField(upload_to='avatars/', blank=True, null=True,default='/static/img/user.jpg', verbose_name='头像')
+    email = models.EmailField(unique=False, null=True, blank=True, default='email@email.com', verbose_name='邮箱')
+    phone = models.CharField(max_length=20, null=True, blank=True, verbose_name='手机号')
+    address = models.CharField(max_length=200, null=True, blank=True, verbose_name='地址')
+    
+    # 注册信息
     create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
     create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
     update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
     update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
 
 
@@ -34,6 +59,13 @@ class Users(models.Model):
 
 
     def __str__(self):
     def __str__(self):
         return f"{self.name}(ID:{self.user_id})"
         return f"{self.name}(ID:{self.user_id})"
+    # 如果变更名称,同步修改User表中的username
+    def save(self, *args, **kwargs):
+        if self.name != self.user_name.username:
+            self.user_name.username = self.name
+            self.user_name.save()
+        super().save(*args, **kwargs)
+
 
 
 class AcademicProfile(models.Model):
 class AcademicProfile(models.Model):
     """学术信息扩展模型 (与用户一对一关联)"""
     """学术信息扩展模型 (与用户一对一关联)"""

+ 24 - 2
userprofile/serializers.py

@@ -1,13 +1,15 @@
 from rest_framework import serializers
 from rest_framework import serializers
 from .models import Users, AcademicProfile, ResearchGroup, GroupMembership
 from .models import Users, AcademicProfile, ResearchGroup, GroupMembership
 
 
+
 class UsersGetSerializer(serializers.ModelSerializer):
 class UsersGetSerializer(serializers.ModelSerializer):
    
    
     roles = serializers.SerializerMethodField()
     roles = serializers.SerializerMethodField()
     introduction = serializers.SerializerMethodField()
     introduction = serializers.SerializerMethodField()
+    avatar_url = serializers.SerializerMethodField()
     class Meta:
     class Meta:
         model = Users
         model = Users
-        fields = ['user_id','name', 'roles','avatar', 'introduction','create_time', 'update_time']
+        fields = ['user_id','name', 'roles','avatar_url', 'introduction','create_time', 'update_time','email', 'phone', 'address']
 
 
     def get_roles(self, obj):
     def get_roles(self, obj):
 
 
@@ -15,4 +17,24 @@ class UsersGetSerializer(serializers.ModelSerializer):
         return ['admin']
         return ['admin']
 
 
     def get_introduction(self, obj):
     def get_introduction(self, obj):
-        return 'I am a super administrator'
+        return 'I am a super administrator'
+    
+    def get_avatar_url(self, obj):
+        if obj.avatar:
+            # 返回绝对URL
+            request = self.context.get('request')
+            return request.build_absolute_uri(obj.avatar.url) 
+        return None
+    
+class UsersPostSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Users
+        fields = ['name', 'email', 'phone', 'address']
+
+
+
+class AcademicProfileGetSerializer(serializers.ModelSerializer):
+    user = UsersGetSerializer(read_only=True)
+    class Meta:
+        model = AcademicProfile
+        fields = ['user', 'degree', 'department', 'university', 'advisor', 'thesis_title', 'thesis_abstract', 'thesis_keywords']

+ 4 - 2
userprofile/urls.py

@@ -3,8 +3,10 @@ from . import views
 
 
 urlpatterns=[
 urlpatterns=[
 
 
-path('profile/', views.UserprofileViewSet.as_view({'get': 'list'}), name='groups'),
+path('profile/', views.UserprofileViewSet.as_view({'get': 'list','post': 'update'}), name='groups'),
+re_path(r'^profile/(?P<pk>[0-9]+)/$', views.UserprofileViewSet.as_view({'get': 'retrieve', 'post': 'update', 'patch': 'partial_update', 'delete': 'destroy'}), name='groups'),
+
+path('avatar-upload/', views.AvatarUploadView.as_view(), name='groups')
 
 
-re_path(r'^profile/(?P<pk>[0-9]+)/$', views.UserprofileViewSet.as_view({'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy'}), name='groups')
 
 
 ]
 ]

+ 166 - 15
userprofile/views.py

@@ -2,11 +2,18 @@ from rest_framework import viewsets
 from utils.page import MyPageNumberPagination
 from utils.page import MyPageNumberPagination
 from rest_framework.filters import OrderingFilter
 from rest_framework.filters import OrderingFilter
 from django_filters.rest_framework import DjangoFilterBackend
 from django_filters.rest_framework import DjangoFilterBackend
+from rest_framework.response import Response
+from rest_framework import status
+from rest_framework.parsers import MultiPartParser, FormParser
+from rest_framework.views import APIView
+import os
+import uuid
+from django.conf import settings
 
 
 from .models import Users, AcademicProfile, ResearchGroup, GroupMembership
 from .models import Users, AcademicProfile, ResearchGroup, GroupMembership
 from .filter import UsersFilter, AcademicProfileFilter, ResearchGroupFilter, GroupMembershipFilter
 from .filter import UsersFilter, AcademicProfileFilter, ResearchGroupFilter, GroupMembershipFilter
-from .serializers import UsersGetSerializer
-
+from .serializers import UsersGetSerializer,UsersPostSerializer
+from .serializers import AcademicProfileGetSerializer
 
 
 class UserprofileViewSet(viewsets.ModelViewSet):
 class UserprofileViewSet(viewsets.ModelViewSet):
     """
     """
@@ -36,11 +43,21 @@ class UserprofileViewSet(viewsets.ModelViewSet):
 
 
     def get_project(self):
     def get_project(self):
         try:
         try:
-            id = self.kwargs.get('pk')
-            return id
+            # 表格通道
+            if 'pk' in self.kwargs:
+                id = self.kwargs.get('pk')
+                return id
+            else:
+            # 用户通道
+                user = self.request.auth
+                if user.roles == 'USR':
+                    return user.user_id
+                else:
+                    return None
         except:
         except:
             return None
             return None
 
 
+
     def get_queryset(self):
     def get_queryset(self):
         project_id = self.get_project()
         project_id = self.get_project()
         if project_id:
         if project_id:
@@ -51,28 +68,53 @@ class UserprofileViewSet(viewsets.ModelViewSet):
     def get_serializer_class(self):
     def get_serializer_class(self):
         if self.action in ['list','retrieve']:
         if self.action in ['list','retrieve']:
             return UsersGetSerializer
             return UsersGetSerializer
+        elif self.action in ['create', 'update', 'partial_update']:
+            return UsersPostSerializer
         else:
         else:
             return UsersGetSerializer
             return UsersGetSerializer
         
         
     def create(self, request, *args, **kwargs):
     def create(self, request, *args, **kwargs):
         serializer = self.get_serializer(data=request.data)
         serializer = self.get_serializer(data=request.data)
         serializer.is_valid(raise_exception=True)
         serializer.is_valid(raise_exception=True)
-        self.perform_create(serializer)
+        serializer.save()
+
         headers = self.get_success_headers(serializer.data)
         headers = self.get_success_headers(serializer.data)
         return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
         return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
     
     
+    # 表格的写法
     def update(self, request, *args, **kwargs):
     def update(self, request, *args, **kwargs):
-        qs = self.get_object()
-        serializer = self.get_serializer(qs, data=request.data, partial=True)
-        serializer.is_valid(raise_exception=True)
-        self.perform_update(serializer)
-        return Response(serializer.data)    
+        if 'pk' in self.kwargs:
+
+            qs = self.get_object()
+            serializer = self.get_serializer(qs, data=request.data, partial=True)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            return Response(serializer.data, status=status.HTTP_200_OK, headers=self.get_success_headers(serializer.data))
+        else:
+            # 没有qs,应该是管理员更新自己的信息
+            qs = request.auth
+            serializer = self.get_serializer(qs, data=request.data, partial=True)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            return Response(serializer.data, status=status.HTTP_200_OK, headers=self.get_success_headers(serializer.data))
+        
+ 
+
     
     
+    # 
+    # def update(self, request, *args, **kwargs):
+    #     qs = request.auth
+    #     serializer = self.get_serializer(qs, data=request.data, partial=True)
+    #     serializer.is_valid(raise_exception=True)
+    #     serializer.save()
+    #     return Response(serializer.data, status=status.HTTP_200_OK, headers=self.get_success_headers(serializer.data))
+
     def partial_update(self, request, *args, **kwargs):
     def partial_update(self, request, *args, **kwargs):
         qs = self.get_object()
         qs = self.get_object()
         serializer = self.get_serializer(qs, data=request.data, partial=True)
         serializer = self.get_serializer(qs, data=request.data, partial=True)
         serializer.is_valid(raise_exception=True)
         serializer.is_valid(raise_exception=True)
-        self.perform_update(serializer)
+        serializer.save()
+
         return Response(serializer.data)
         return Response(serializer.data)
     
     
     def destroy(self, request, *args, **kwargs):
     def destroy(self, request, *args, **kwargs):
@@ -81,10 +123,119 @@ class UserprofileViewSet(viewsets.ModelViewSet):
         qs.save()
         qs.save()
         return Response(status=status.HTTP_204_NO_CONTENT)
         return Response(status=status.HTTP_204_NO_CONTENT)
     
     
-    def perform_create(self, serializer):
-        serializer.save()
+
+    
+class AvatarUploadView(APIView):
+    parser_classes = [MultiPartParser]
     
     
-    def perform_update(self, serializer):
+    def post(self, request):
+        avatar_file = request.FILES.get('avatar')
+        if not avatar_file:
+            return Response({"error": "未上传头像文件"}, status=400)
+        
+        # 生成唯一文件名
+        ext = os.path.splitext(avatar_file.name)[1]
+        filename = f"{uuid.uuid4().hex}{ext}"
+        
+        # 目标目录
+        avatars_dir = os.path.join(settings.MEDIA_ROOT, 'avatars')
+        
+        # 确保目录存在
+        if not os.path.exists(avatars_dir):
+            os.makedirs(avatars_dir)
+        
+        # 保存文件
+        save_path = os.path.join(avatars_dir, filename)
+        with open(save_path, 'wb+') as destination:
+            for chunk in avatar_file.chunks():
+                destination.write(chunk)
+        
+        # 返回相对路径(前端会拼接完整URL)
+        avatar_url = f"/avatars/{filename}"
+        
+        # 更新用户头像
+        user = request.auth
+        user.avatar = avatar_url
+        user.save()
+        
+        # 传递 request 到序列化器上下文
+        serializer = UsersGetSerializer(user, context={'request': request})
+        return Response(serializer.data)
+
+class AcademicProfileViewSet(viewsets.ModelViewSet):
+    """
+    学术资料视图集
+        retrieve:
+            Response a data list (get)
+
+        list:
+            Response a data list (all)
+
+        create:
+            Create a data line (post)
+
+        delete:
+            Delete a data line (delete)
+
+        partial_update:
+            Partial_update a data (patch:partial_update)
+
+        update:
+            Update a data (put:update)
+    """
+    fliter_backends = (DjangoFilterBackend, OrderingFilter)
+    ordering_fields = ('create_time', 'update_time')
+    filterset_class = AcademicProfileFilter
+    pagination_class = MyPageNumberPagination
+
+    def get_project(self):
+        try:
+            id = self.kwargs.get('pk')
+            return id
+        except:
+            return None
+
+    def get_queryset(self):
+        project_id = self.get_project()
+        if project_id:
+            return AcademicProfile.objects.filter(user_id=project_id)
+        else:
+            return AcademicProfile.objects.all()
+
+    def get_serializer_class(self):
+        if self.action in ['list','retrieve']:
+            return UsersGetSerializer
+        elif self.action in ['create', 'update', 'partial_update']:
+            return UsersPostSerializer
+        else:
+            return UsersGetSerializer
+        
+    def create(self, request, *args, **kwargs):
+        serializer = self.get_serializer(data=request.data)
+        serializer.is_valid(raise_exception=True)
         serializer.save()
         serializer.save()
+
+        headers = self.get_success_headers(serializer.data)
+        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
     
     
- 
+    # 表格的写法
+    # def update(self, request, *args, **kwargs):
+    #     qs = self.get_object()
+    #     serializer = self.get_serializer(qs, data=request.data, partial=True)
+    #     serializer.is_valid(raise_exception=True)
+    #     self.perform_update(serializer)
+    #     return Response(serializer.data)   
+    # 
+    def update(self, request, *args, **kwargs):
+        qs = request.auth
+        serializer = self.get_serializer(qs, data=request.data, partial=True)
+        serializer.is_valid(raise_exception=True)
+        serializer.save()
+        return Response(serializer.data, status=status.HTTP_200_OK, headers=self.get_success_headers(serializer.data))
+
+    def partial_update(self, request, *args, **kwargs):
+        qs = self.get_object()
+        serializer = self.get_serializer(qs, data=request.data, partial=True)
+        serializer.is_valid(raise_exception=True)
+        serializer.save()
+

+ 12 - 8
userregister/views.py

@@ -64,18 +64,22 @@ def register(request, *args, **kwargs):
                             # 后续开发者用户需要手动修改用户信息
                             # 后续开发者用户需要手动修改用户信息
                             if Users.objects.filter().count() == 0:
                             if Users.objects.filter().count() == 0:
                                 userzl = User.objects.create_user(username='adminzl',password=str(123456))
                                 userzl = User.objects.create_user(username='adminzl',password=str(123456))
-                                Users.objects.create(user_id=userzl.id, name='adminzl',
-                                                 openid="adminzl", appid="adminzl",
-                                                 t_code=Md5.md5(str(timezone.now())),
-                                                 developer=1, ip=ip)
+                                Users.objects.create(
+                                                user_name = userzl,
+                                                user_id=userzl.id, name='adminzl',
+                                                openid="adminzl", appid="adminzl",
+                                                t_code=Md5.md5(str(timezone.now())),
+                                                developer=1, ip=ip)
                                 
                                 
                             transaction_code = Md5.md5(data['name'])
                             transaction_code = Md5.md5(data['name'])
                             user = User.objects.create_user(username=str(data['name']),
                             user = User.objects.create_user(username=str(data['name']),
                                                             password=str(data['password1']))
                                                             password=str(data['password1']))
-                            Users.objects.create(user_id=user.id, name=str(data['name']),
-                                                 openid=transaction_code, appid=Md5.md5(data['name'] + '1'),
-                                                 t_code=Md5.md5(str(timezone.now())),
-                                                 developer=1, ip=ip)
+                            Users.objects.create(
+                                                user_name = user,
+                                                user_id=user.id, name=str(data['name']),
+                                                openid=transaction_code, appid=Md5.md5(data['name'] + '1'),
+                                                t_code=Md5.md5(str(timezone.now())),
+                                                developer=1, ip=ip)
                             auth.login(request, user)
                             auth.login(request, user)