Browse Source

stash

master
kola-web 2 weeks ago
parent
commit
4a3b12f59d
  1. 7
      README.md
  2. 11
      project.private.config.json
  3. 5
      src/api/request.ts
  4. 3
      src/app.json
  5. 166
      src/app.ts
  6. 89
      src/components/svg-icon/README.md
  7. 200
      src/components/svg-icon/index.ts
  8. 1
      src/components/svg-icon/index.wxml
  9. BIN
      src/images/icon69.png
  10. BIN
      src/images/icon70.png
  11. BIN
      src/images/icon71.png
  12. BIN
      src/images/icon72.png
  13. BIN
      src/images/icon73.png
  14. BIN
      src/images/icon74.png
  15. BIN
      src/images/icon75.png
  16. 5
      src/images/svg2.svg
  17. 1
      src/pages/login/index.json
  18. 26
      src/pages/login/index.scss
  19. 148
      src/pages/login/index.ts
  20. 24
      src/pages/login/index.wxml
  21. 7
      src/pages/schedule/index.json
  22. 310
      src/pages/schedule/index.scss
  23. 149
      src/pages/schedule/index.ts
  24. 142
      src/pages/schedule/index.wxml
  25. 29
      typings/index.d.ts

7
README.md

@ -6,3 +6,10 @@ powershell 软链形式
``` ```
New-Item -ItemType Junction -Path "src/images" -Target C:\Users\kola\project\school-system\web_dist\images New-Item -ItemType Junction -Path "src/images" -Target C:\Users\kola\project\school-system\web_dist\images
``` ```
测试账号
```
账号 2026050122123
密码 123456
```

11
project.private.config.json

@ -4,13 +4,20 @@
"miniprogram": { "miniprogram": {
"list": [ "list": [
{ {
"name": "登录", "name": "课表",
"pathName": "pages/login/index", "pathName": "pages/schedule/index",
"query": "", "query": "",
"scene": null, "scene": null,
"launchMode": "default" "launchMode": "default"
}, },
{ {
"name": "登录",
"pathName": "pages/login/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "我的评论", "name": "我的评论",
"pathName": "pages/myComment/index", "pathName": "pages/myComment/index",
"query": "", "query": "",

5
src/api/request.ts

@ -24,15 +24,16 @@ export const request = function (
mask: true, mask: true,
}) })
} }
const app = getApp<IAppOption>()
wx.request({ wx.request({
header: { header: {
loginState: getApp().globalData.loginState,
...header, ...header,
}, },
url: gUrl + url, url: gUrl + url,
method, method,
data: { data: {
loginState: getApp().globalData.loginState,
...(data as object), ...(data as object),
}, },
...options, ...options,

3
src/app.json

@ -18,7 +18,8 @@
"pages/agentEva/index", "pages/agentEva/index",
"pages/myAct/index", "pages/myAct/index",
"pages/myAgent/index", "pages/myAgent/index",
"pages/myComment/index" "pages/myComment/index",
"pages/schedule/index"
], ],
"window": { "window": {
"backgroundTextStyle": "light", "backgroundTextStyle": "light",

166
src/app.ts

@ -1,9 +1,9 @@
/* eslint-disable perfectionist/sort-imports */
import dayjs from 'dayjs'
import page from '@/utils/page' import page from '@/utils/page'
import { request } from './api/request' import { request } from './api/request'
import { parseScene } from './utils/util' import { parseScene } from './utils/util'
const dayjs = require('dayjs')
const licia = require('miniprogram-licia') const licia = require('miniprogram-licia')
require('/utils/dayjs/day-zh-cn.js') require('/utils/dayjs/day-zh-cn.js')
const relativeTime = require('/utils/dayjs/relativeTime.js') const relativeTime = require('/utils/dayjs/relativeTime.js')
@ -12,8 +12,8 @@ dayjs.extend(relativeTime)
App<IAppOption>({ App<IAppOption>({
globalData: { globalData: {
url: '', url: 'https://app.gohighedu.cn',
upFileUrl: '', upFileUrl: 'https://app.gohighedu.cn',
imageUrl: 'https://app.gohighedu.cn/images', imageUrl: 'https://app.gohighedu.cn/images',
Timestamp: new Date().getTime(), Timestamp: new Date().getTime(),
@ -21,9 +21,7 @@ App<IAppOption>({
waitBindDoctorId: '', waitBindDoctorId: '',
scene: {}, scene: {},
loginState: '',
initLoginInfo: {}, initLoginInfo: {},
userInfo: {}, userInfo: {},
}, },
onLaunch() { onLaunch() {
@ -42,19 +40,44 @@ App<IAppOption>({
startLogin(callback?: () => void) { startLogin(callback?: () => void) {
wx.login({ wx.login({
success: (res) => { success: (res) => {
wx.ajax({ console.log("DEBUGPRINT[244]: app.ts:42: res=", res)
method: 'GET', // // 调用静默登录接口
url: '?r=wtx/user/init-login', // wx.ajax({
data: { // method: 'POST',
code: res.code, // url: '/auth/silent-login',
// showMsg: false, // 隐藏错误提示
// data: {
// code: res.code,
// },
// })
// .then((response: any) => {
// const { accessToken, user } = response
//
// // 存储 accessToken
// this.globalData.accessToken = accessToken
//
// // 存储用户信息
// if (user) {
// this.globalData.userInfo = user
// }
//
// // 更新 initLoginInfo
// this.globalData.initLoginInfo = {
// user,
// }
//
// if (callback) {
// callback()
// }
// })
// .catch((err: any) => {
// // 静默失败,不提示用户
// console.error('静默登录请求失败:', err)
// })
}, },
}).then((res: any) => { fail: (err) => {
this.globalData.loginState = res.loginState // 静默失败,不提示用户
this.globalData.initLoginInfo = res console.error('wx.login 失败:', err)
if (callback) {
callback()
}
})
}, },
}) })
}, },
@ -71,57 +94,95 @@ App<IAppOption>({
}) })
}, },
waitLogin({ type }: { type?: 0 | 1 | 2 | 'any' } = { type: 'any' }) { waitLogin({ type }: { type?: 0 | 1 | 2 | 'any' } = { type: 'any' }) {
return new Promise<void>((resolve) => { return new Promise<void>((resolve, reject) => {
const checkLogin = () => { const checkLogin = () => {
if (this.globalData.loginState) { // type = 0:不需要登录即可访问
if (this.checkLoginType(type ?? 'any')) { if (type === 0) {
resolve() resolve()
return
} }
// type = 'any':不检查登录状态
if (type === 'any') {
resolve()
return return
} }
setTimeout(() => {
checkLogin() // type = 1 或 2:需要登录
}, 500) if (type === 1 || type === 2) {
// 检查是否有 accessToken
if (this.globalData.accessToken) {
// 已登录,检查是否需要绑定
if (this.globalData.initLoginInfo?.needBind) {
// 需要绑定,跳转到登录页
this.redirectToLogin(type)
reject(new Error('need_bind'))
return
}
resolve()
return
}
// 未登录,跳转到登录页
this.redirectToLogin(type)
reject(new Error('not_logged_in'))
return
}
resolve()
} }
checkLogin() checkLogin()
}) })
}, },
checkLoginType(type: 0 | 1 | 2 | 'any') {
const { loginType, isLogin, isReg } = this.globalData.initLoginInfo
if (type === 'any') { /**
return true *
} */
redirectToLogin(_type: 1 | 2) {
// 获取当前页面路径
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentUrl = currentPage ? currentPage.route : ''
if (isLogin !== 1) { // 记录来源页面,登录后返回
if (type === 0) { if (currentUrl && currentUrl !== 'pages/login/index') {
return true this.globalData.loginRedirectUrl = currentUrl
} }
wx.reLaunch({ wx.reLaunch({
url: '/pages/index/index', url: '/pages/login/index',
}) })
return false },
checkLoginType(type: 0 | 1 | 2 | 'any') {
// type = 0:不需要登录
if (type === 0) {
return true
} }
if (isReg !== 1) { // type = 'any':不检查
const typePageUrl: Record<number, string> = { if (type === 'any') {
1: '/pages/login/index', return true
2: '/pages/login/index',
} }
wx.reLaunch({
url: typePageUrl[loginType as 1 | 2], // type = 1 或 2:需要登录
}) if (type === 1 || type === 2) {
// 检查是否有 accessToken
if (!this.globalData.accessToken) {
this.redirectToLogin(type as 1 | 2)
return false return false
} }
if (loginType !== type) { // 检查是否需要绑定
wx.reLaunch({ if (this.globalData.initLoginInfo?.needBind) {
url: '/pages/index/index', this.redirectToLogin(type as 1 | 2)
})
return false return false
} }
return true return true
}
return true
}, },
getUserInfo(type: 0 | 1 | 2 = 0) { getUserInfo(type: 0 | 1 | 2 = 0) {
const url: Record<number, string> = { const url: Record<number, string> = {
@ -135,7 +196,6 @@ App<IAppOption>({
}) })
}, },
autoUpdate() { autoUpdate() {
const self = this
if (wx.canIUse('getUpdateManager')) { if (wx.canIUse('getUpdateManager')) {
const updateManager = wx.getUpdateManager() const updateManager = wx.getUpdateManager()
updateManager.onCheckForUpdate((res) => { updateManager.onCheckForUpdate((res) => {
@ -143,18 +203,18 @@ App<IAppOption>({
wx.showModal({ wx.showModal({
title: '更新提示', title: '更新提示',
content: '检测到新版本,是否下载新版本并重启小程序?', content: '检测到新版本,是否下载新版本并重启小程序?',
success(res) { success: (res) => {
if (res.confirm) { if (res.confirm) {
self.downLoadAndUpdate(updateManager) this.downLoadAndUpdate(updateManager)
} else if (res.cancel) { } else if (res.cancel) {
wx.showModal({ wx.showModal({
title: '温馨提示~', title: '温馨提示~',
content: '本次版本更新涉及到新的功能添加,旧版本无法正常访问的哦~', content: '本次版本更新涉及到新的功能添加,旧版本无法正常访问的哦~',
showCancel: false, showCancel: false,
confirmText: '确定更新', confirmText: '确定更新',
success(res) { success: (res) => {
if (res.confirm) { if (res.confirm) {
self.downLoadAndUpdate(updateManager) this.downLoadAndUpdate(updateManager)
} }
}, },
}) })
@ -165,8 +225,8 @@ App<IAppOption>({
}) })
} else { } else {
wx.showModal({ wx.showModal({
title: '提示', title: '温馨提示',
content: '当前微信版本过低,无法使用功能,请升级到最新微信版本后重试。', content: '当前微信版本过低,无法使用版本更新功能,请升级到最新微信版本后重试。',
}) })
} }
}, },

89
src/components/svg-icon/README.md

@ -26,96 +26,32 @@
<!-- 指定缩放模式 --> <!-- 指定缩放模式 -->
<svg-icon src="/images/icon.svg" mode="widthFix" /> <svg-icon src="/images/icon.svg" mode="widthFix" />
<!-- 设置宽高 -->
<svg-icon src="/images/icon.svg" width="32rpx" height="32rpx" />
<svg-icon src="/images/icon.svg" width="64px" height="64px" />
``` ```
## 单颜色重新着色 ## 重新着色
传入 `color` 时,会对 SVG 中所有声明 `fill/stroke` 的元素统一着色: 传入 `color` 时,会对 SVG 中所有声明 `fill/stroke` 的元素统一着色:
```xml ```xml
<!-- 单一颜色改色 -->
<svg-icon src="/images/icon.svg" color="#ff0000" /> <svg-icon src="/images/icon.svg" color="#ff0000" />
``` ```
## 多颜色重新着色
### 数组形式(按顺序替换)
以数组形式传入 `colors` 时,依照数组中的颜色顺序,对 SVG 中所有声明 `fill/stroke` 的元素按顺序重新着色:
```ts
// page.ts
Page({
data: {
colorsArray: ['#ff0000', '#00ff00', '#0000ff'],
},
})
```
```xml
<!-- page.wxml -->
<svg-icon src="/images/icon.svg" colors="{{colorsArray}}" />
```
### 对象形式(按键值关系替换)
以对象形式传入 `colors` 时,依照对象中的键值关系,对 SVG 中所有声明 `fill/stroke` 的元素按对应关系重新着色:
```ts
// page.ts
Page({
data: {
colorsObject: {
black: '#ff0000',
'#fff': '#00ff00',
'#808080': '#cdcdcd',
},
},
})
```
```xml
<!-- page.wxml -->
<svg-icon src="/images/icon.svg" colors="{{colorsObject}}" />
```
## 组合重新着色
同时传入 `color``colors` 组合搭配,既能为指定元素重新着色,也能为其余未指定元素统一着色:
```ts
// page.ts
Page({
data: {
colorsArray: ['#ff0000', '#00ff00'],
},
})
```
```xml
<!-- page.wxml -->
<svg-icon src="/images/icon.svg" color="#0000ff" colors="{{colorsArray}}" />
```
## 网络资源
支持传入网络 SVG 资源地址:
```xml
<svg-icon src="https://example.com/icon.svg" color="#ff0000" />
```
**注意**:当 src 传入网络资源并重新着色时,请将网络资源的域名配置于小程序的 `downloadFile` 合法域名中。
## API ## API
### Properties ### Properties
| 参数 | 说明 | 类型 | 默认值 | 必填 | | 参数 | 说明 | 类型 | 默认值 | 必填 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `src` | SVG 资源地址(支持本地路径、临时路径、网络资源) | `string` | `''` | 是 | | `src` | SVG 资源地址(支持本地路径) | `string` | `''` | 是 |
| `color` | SVG 单一颜色(对所有 fill/stroke 元素统一着色) | `string` | `''` | 否 | | `color` | SVG 颜色(对所有 fill/stroke 元素统一着色) | `string` | `''` | 否 |
| `colors` | SVG 多颜色配置(支持数组或对象) | `array \| object` | `null` | 否 |
| `mode` | SVG 裁剪、缩放模式(与 `image` 标签相同) | `string` | `''` | 否 | | `mode` | SVG 裁剪、缩放模式(与 `image` 标签相同) | `string` | `''` | 否 |
| `width` | SVG 宽度(支持 rpx/px 单位) | `string` | `'48rpx'` | 否 |
| `height` | SVG 高度(支持 rpx/px 单位) | `string` | `'48rpx'` | 否 |
### Events ### Events
@ -153,12 +89,15 @@ Page({
## 实现原理 ## 实现原理
1. 读取 SVG 文件内容(本地文件或网络文件) 1. 读取 SVG 文件内容
2. 通过正则替换 SVG 中的 `fill/stroke` 属性值来实现改色 2. 通过正则替换 SVG 中的 `fill/stroke` 属性值来实现改色
3. 将修改后的 SVG 内容转为 base64 格式,作为 `image``src` 3. 将修改后的 SVG 内容转为 base64 格式,作为 `image``src`
## 注意事项 ## 注意事项
- SVG 文件必须包含 `fill``stroke` 属性才能被改色 - SVG 文件必须包含 `fill``stroke` 属性才能被改色
- 支持本地路径、网络地址(HTTP/HTTPS)、临时路径(wxfile://)
- **开发工具限制**:由于开发工具的文件系统权限限制,本地路径的 SVG 可能无法改色,此时会直接显示原 SVG(不改色)
- **正式环境建议**:使用网络地址,这样可以正常改色
- 网络资源需要配置 `downloadFile` 合法域名 - 网络资源需要配置 `downloadFile` 合法域名
- 组件会缓存下载的网络资源,避免重复下载 - 组件会缓存下载的网络资源,避免重复下载

200
src/components/svg-icon/index.ts

@ -7,22 +7,90 @@ import { encode } from './base64'
const fs = wx.getFileSystemManager() const fs = wx.getFileSystemManager()
// 临时文件缓存(网络资源下载后缓存 // 网络资源缓存(避免重复下载)
const tempFileMap = new Map<string, string>() const networkCache = new Map<string, string>()
/** /**
* *
* @param url
* @returns
*/ */
function downloadFileSync(url: string): Promise<WechatMiniprogram.DownloadFileSuccessCallbackResult> { function isNetworkUrl(src: string): boolean {
return new Promise((resolve, reject) => { return /^https?:\/\//.test(src)
}
/**
*
*/
function isLocalPath(src: string): boolean {
return !isNetworkUrl(src) && !src.startsWith('wxfile:')
}
/**
*
* 使
*/
function getLocalFullPath(src: string): string {
// 如果已经是完整路径,直接返回
if (src.startsWith('/') && !src.startsWith('/images')) {
return src
}
// 微信小程序中,/images/xxx 是相对于项目根目录的
// 开发工具中需要转换为完整路径
// 注意:这里假设项目根目录为 src/
if (src.startsWith('/images/')) {
// 尝试多种路径格式
const paths = [
src, // 原始路径
`/src${src}`, // 添加 src 前缀
`${wx.env.USER_DATA_PATH}${src}`, // 用户数据路径
]
// 尝试读取文件,找到正确的路径
for (const path of paths) {
try {
fs.accessSync(path)
return path
} catch {
continue
}
}
// 如果都找不到,返回原始路径
return src
}
return src
}
/**
*
*/
async function downloadNetworkResource(url: string): Promise<string> {
// 检查缓存
const cachedPath = networkCache.get(url)
if (cachedPath) {
try {
fs.accessSync(cachedPath)
return cachedPath
} catch {
// 缓存失效,重新下载
networkCache.delete(url)
}
}
// 下载文件
const downloadResult = await new Promise<WechatMiniprogram.DownloadFileSuccessCallbackResult>((resolve, reject) => {
wx.downloadFile({ wx.downloadFile({
url, url,
success: resolve, success: resolve,
fail: reject, fail: reject,
}) })
}) })
// 缓存临时文件路径
networkCache.set(url, downloadResult.tempFilePath)
return downloadResult.tempFilePath
} }
Component({ Component({
@ -33,100 +101,102 @@ Component({
externalClasses: ['image-class'], externalClasses: ['image-class'],
properties: { properties: {
/** SVG 资源地址(支持本地路径、临时路径、网络资源) */ /** SVG 资源地址(支持本地路径、网络地址、临时路径) */
src: { src: {
type: String, type: String,
value: '', value: '',
}, },
/** SVG 单一颜色(对所有 fill/stroke 元素统一着色) */ /** SVG 颜色(对所有 fill/stroke 元素统一着色) */
color: { color: {
type: String, type: String,
value: '', value: '',
}, },
/** SVG 多颜色配置(支持数组或对象) */
colors: {
type: null,
value: null,
},
/** SVG 裁剪、缩放模式(与 image 标签相同) */ /** SVG 裁剪、缩放模式(与 image 标签相同) */
mode: { mode: {
type: String, type: String,
value: '', value: '',
}, },
/** SVG 宽度(支持 rpx/px 单位,默认 48rpx) */
width: {
type: String,
value: '48rpx',
},
/** SVG 高度(支持 rpx/px 单位,默认 48rpx) */
height: {
type: String,
value: '48rpx',
},
}, },
observers: { observers: {
'src, color, colors': async function (src: string, color: string, colors: unknown) { 'src, color': async function (src: string, color: string) {
try { try {
// 如果需要改色 if (!src) {
if (color || (colors && ((Array.isArray(colors) && colors.length > 0) || (typeof colors === 'object' && Object.keys(colors).length > 0)))) { this.setData({ base64: '' })
return
}
// 如果不需要改色,直接使用原路径
if (!color) {
this.setData({ base64: src })
return
}
// 获取 SVG 内容
let svgData: string let svgData: string
let filePath: string
if (isNetworkUrl(src)) {
// 网络资源:先下载到本地
filePath = await downloadNetworkResource(src)
svgData = fs.readFileSync(filePath, 'utf8') as string
} else if (isLocalPath(src)) {
// 本地资源:获取完整路径
filePath = getLocalFullPath(src)
// 判断是否为网络资源(排除开发工具临时路径 http://tmp/) // 尝试读取文件
if (/^https?:\/\//.test(src) && !/^http:\/\/tmp\//.test(src)) {
// 网络资源需要先下载
let tempFilePath = tempFileMap.get(src)
try { try {
if (!tempFilePath) throw new Error('未缓存') svgData = fs.readFileSync(filePath, 'utf8') as string
// 检查临时文件是否存在 } catch (readErr) {
fs.accessSync(tempFilePath) // 如果读取失败(权限问题),直接使用原路径
} catch { console.warn('SVG 本地文件读取失败,使用原路径:', readErr)
// 下载文件 this.setData({ base64: src })
const downloadResult = await downloadFileSync(src) return
tempFilePath = downloadResult.tempFilePath
// 缓存临时文件路径
tempFileMap.set(src, tempFilePath)
} }
// 读取文件内容
svgData = fs.readFileSync(tempFilePath, 'utf8') as string
} else { } else {
// 本地资源直接读取 // 临时路径:直接读取
try {
svgData = fs.readFileSync(src, 'utf8') as string svgData = fs.readFileSync(src, 'utf8') as string
} catch (readErr) {
console.warn('SVG 临时文件读取失败,使用原路径:', readErr)
this.setData({ base64: src })
return
} }
// 处理颜色配置
const colorsConfig = colors || {}
// 替换 SVG 中的 fill/stroke 属性
if (/(?:fill|stroke)=".*?"/.test(svgData)) {
let colorIndex = 0
svgData = svgData.replace(/(?:fill|stroke)=".*?"/g, (matched) => {
// 获取原本颜色值
const originalColor = matched.slice(matched.indexOf('"') + 1, -1)
// 计算替换颜色
let replaceColor: string
if (Array.isArray(colorsConfig)) {
// 数组形式:按顺序替换
replaceColor = colorsConfig[colorIndex++] || color || originalColor
} else {
// 对象形式:按键值关系替换
replaceColor = colorsConfig[originalColor] || colorsConfig[colorIndex++] || color || originalColor
} }
// 返回替换后的属性 // 替换 SVG 中的 fill/stroke 属性
if (/fill/.test(matched)) return `fill="${replaceColor}"` let modifiedSvg = svgData
if (/stroke/.test(matched)) return `stroke="${replaceColor}"` if (/(?:fill|stroke)="[^"]*"/.test(modifiedSvg)) {
return `fill="${replaceColor}"` modifiedSvg = modifiedSvg.replace(/(?:fill|stroke)="[^"]*"/g, (matched) => {
if (/fill/.test(matched)) return `fill="${color}"`
if (/stroke/.test(matched)) return `stroke="${color}"`
return `fill="${color}"`
}) })
} }
// 设置默认底色(SVG 根元素) // 设置 SVG 根元素的默认底色
const defaultColor = (typeof colorsConfig === 'object' && !Array.isArray(colorsConfig) && (colorsConfig['#000'] || colorsConfig['#000000'] || colorsConfig.black)) || color if (!/fill="[^"]*"/.test(modifiedSvg.slice(0, modifiedSvg.indexOf('>')))) {
if (defaultColor && !/fill=".*?"/.test(svgData.slice(0, svgData.indexOf('>')))) { modifiedSvg = modifiedSvg.replace(/<svg /, `<svg fill="${color}" `)
svgData = svgData.replace(/<svg /, `<svg fill="${defaultColor}" `)
} }
// 转为 base64 格式 // 转为 base64 格式
this.setData({ this.setData({
base64: `data:image/svg+xml;base64,${encode(svgData)}`, base64: `data:image/svg+xml;base64,${encode(modifiedSvg)}`,
}) })
} else {
// 不需要改色,直接使用原路径
this.setData({ base64: src })
}
} catch (err) { } catch (err) {
console.error('SVG 加载失败:', err) console.error('SVG 加载失败:', err)
// 如果改色失败,直接使用原路径
this.setData({ base64: src })
this.triggerEvent('error', err) this.triggerEvent('error', err)
} }
}, },

1
src/components/svg-icon/index.wxml

@ -6,6 +6,7 @@
class="svg-icon image-class" class="svg-icon image-class"
src="{{base64}}" src="{{base64}}"
mode="{{mode}}" mode="{{mode}}"
style="width: {{width}}; height: {{height}};"
binderror="onImageError" binderror="onImageError"
bindload="onImageLoad" bindload="onImageLoad"
/> />

BIN
src/images/icon69.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/images/icon70.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/images/icon71.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
src/images/icon72.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/images/icon73.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/images/icon74.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/images/icon75.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

5
src/images/svg2.svg

@ -0,0 +1,5 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Container">
<path id="Icon" d="M9 9C11.4737 9 13.4737 7 13.4737 4.52632C13.4737 2.05263 11.4737 0 9 0C6.52632 0 4.52632 2.05263 4.52632 4.52632C4.52632 7 6.52632 9 9 9ZM9 11.2632C6 11.2632 0 12.7368 0 15.7368V16.8421C0 17.4737 0.526316 18 1.15789 18H16.8421C17.4737 18 18 17.4737 18 16.8421V15.7368C18 12.7368 12 11.2632 9 11.2632Z" fill="#671FAB"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 465 B

1
src/pages/login/index.json

@ -1,3 +1,4 @@
{ {
"navigationBarTitleText": "登录",
"usingComponents": {} "usingComponents": {}
} }

26
src/pages/login/index.scss

@ -53,6 +53,7 @@
} }
} }
.input { .input {
flex: 1;
height: 84rpx; height: 84rpx;
font-size: 32rpx; font-size: 32rpx;
color: rgba(71, 85, 105, 1); color: rgba(71, 85, 105, 1);
@ -61,18 +62,37 @@
color: rgba(203, 213, 225, 1); color: rgba(203, 213, 225, 1);
} }
} }
.tip2{ .tip2 {
margin-top: 28rpx; margin-top: 28rpx;
font-size: 28rpx; font-size: 28rpx;
color: rgba(148, 163, 184, 1); color: rgba(148, 163, 184, 1);
line-height: 42rpx; line-height: 42rpx;
} }
.agreement{ .agreement {
margin-top: 42rpx;
font-size: 28rpx; font-size: 28rpx;
color: rgba(71, 85, 105, 1); color: rgba(71, 85, 105, 1);
.high{ .wx-checkbox-input {
margin-top: -10rpx;
width: 29rpx;
height: 29rpx;
border-radius: 50%;
}
.high {
color: rgba(74, 184, 253, 1); color: rgba(74, 184, 253, 1);
} }
} }
.btn {
margin-top: 47rpx;
height: 96rpx;
font-size: 32rpx;
color: rgba(255, 255, 255, 1);
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #9ddffd 0%, #4ab8fd 100%);
box-shadow: 0rpx 15rpx 30rpx -6rpx rgba(74, 172, 219, 0.4);
border-radius: 16rpx 16rpx 16rpx 16rpx;
}
} }
} }

148
src/pages/login/index.ts

@ -1,8 +1,152 @@
const app = getApp<IAppOption>() const app = getApp<IAppOption>()
Page({ Page({
data: {}, data: {
onLoad() {}, checked: false,
account: '',
password: '',
loading: false,
},
onLoad() {
app.waitLogin({ type: 0 }).then(() => {})
},
/**
*
*/
onAccountInput(e: WechatMiniprogram.Input) {
this.setData({
account: e.detail.value,
})
},
/**
*
*/
onPasswordInput(e: WechatMiniprogram.Input) {
this.setData({
password: e.detail.value,
})
},
/**
*
*/
onCheckboxChange() {
this.setData({
checked: !this.data.checked,
})
},
/**
*
*/
onSubmit() {
const { account, password, checked, loading } = this.data
// 防止重复提交
if (loading) {
return
}
// 表单验证
if (!account) {
wx.showToast({
title: '请输入学工号',
icon: 'none',
})
return
}
if (!password) {
wx.showToast({
title: '请输入密码',
icon: 'none',
})
return
}
if (!checked) {
wx.showToast({
title: '请阅读并接受隐私保护指引',
icon: 'none',
})
return
}
// 开始提交
this.setData({ loading: true })
wx.showLoading({
title: '正在绑定...',
mask: true,
})
// 调用 CAS 账号绑定登录接口
wx.ajax({
method: 'POST',
url: '/auth/cas-bind',
data: {
account,
password,
openidSession: app.globalData.accessToken,
protocolVersion: 'v1.0',
},
})
.then((response: any) => {
wx.hideLoading()
this.setData({ loading: false })
const { accessToken, expireIn, needBind, user } = response
// 存储 accessToken
app.globalData.accessToken = accessToken
app.globalData.tokenExpireIn = expireIn
// 存储用户信息
if (user) {
app.globalData.userInfo = user
}
// 更新 initLoginInfo
app.globalData.initLoginInfo = {
needBind,
user,
}
// 绑定成功,跳转到目标页面
wx.showToast({
title: '绑定成功',
icon: 'success',
})
setTimeout(() => {
// 检查是否有来源页面,如果有则返回,否则跳转到首页
const redirectUrl = app.globalData.loginRedirectUrl
if (redirectUrl) {
// 清除记录的来源页面
app.globalData.loginRedirectUrl = ''
wx.reLaunch({
url: `/${redirectUrl}`,
})
} else {
wx.reLaunch({
url: '/pages/index/index',
})
}
}, 1500)
})
.catch((err: any) => {
wx.hideLoading()
this.setData({ loading: false })
console.error('CAS 绑定登录请求失败:', err)
wx.showToast({
title: '网络请求失败',
icon: 'none',
})
})
},
handleGetUserInfo() { handleGetUserInfo() {
wx.getUserProfile({ wx.getUserProfile({
desc: '用于完善用户资料', desc: '用于完善用户资料',

24
src/pages/login/index.wxml

@ -16,15 +16,33 @@
</view> </view>
<view class="form-item"> <view class="form-item">
<view class="label required">账号</view> <view class="label required">账号</view>
<input class="input" placeholder-class="input-palce" type="number" placeholder="请输入学工号" /> <input
class="input"
placeholder-class="input-palce"
type="number"
placeholder="请输入学工号"
value="{{account}}"
bindinput="onAccountInput"
/>
</view> </view>
<view class="form-item"> <view class="form-item">
<view class="label required">密码</view> <view class="label required">密码</view>
<input class="input" placeholder-class="input-palce" type="text" placeholder="请输入密码" /> <input
class="input"
placeholder-class="input-palce"
password
placeholder="请输入密码"
value="{{password}}"
bindinput="onPasswordInput"
/>
</view> </view>
<view class="tip2">SIC+仅将您的密码用于单次身份验证,不会保存您的密码</view> <view class="tip2">SIC+仅将您的密码用于单次身份验证,不会保存您的密码</view>
<view class="agreement"> <view class="agreement">
<radio>我已阅读并接受《深职大SIC+小程序隐私保护指引》</radio> <checkbox color="rgba(74, 184, 253, 1)" checked="{{checked}}" bind:tap="onCheckboxChange">
我已阅读并接受
<text class="high">《深职大SIC+小程序隐私保护指引》</text>
</checkbox>
</view> </view>
<view class="btn" bindtap="onSubmit">提交</view>
</view> </view>
</view> </view>

7
src/pages/schedule/index.json

@ -0,0 +1,7 @@
{
"navigationBarTitleText": "课表",
"usingComponents": {
"van-icon": "@vant/weapp/icon/index",
"svg-icon": "/components/svg-icon/index"
}
}

310
src/pages/schedule/index.scss

@ -0,0 +1,310 @@
.page-title {
font-size: 32rpx;
color: rgba(30, 41, 59, 1);
}
.page-back {
font-size: 32rpx;
color: rgba(30, 41, 59, 1);
}
.page {
padding: 0 30rpx;
.page-header {
padding-top: 30rpx;
display: flex;
align-items: center;
gap: 46rpx;
.week-wrap {
flex: 1;
.week {
display: flex;
align-items: center;
gap: 16rpx;
.icon {
width: 42rpx;
height: 42rpx;
}
.content {
font-size: 38rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
}
.school-year {
margin-top: 8rpx;
font-size: 28rpx;
color: rgba(107, 114, 128, 1);
}
}
.notify {
flex-shrink: 0;
padding: 5rpx 15rpx 5rpx 5rpx;
display: flex;
align-items: center;
gap: 6rpx;
background-color: rgba(247, 248, 250, 1);
border: 1px solid #fff;
border-radius: 50rpx;
font-size: 28rpx;
color: rgba(74, 184, 253, 1);
.icon-wrap {
flex-shrink: 0;
width: 42rpx;
height: 42rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
border-radius: 50rpx;
.icon {
width: 22rpx;
height: 28rpx;
}
}
}
.switch-format {
flex-shrink: 0;
padding: 8rpx;
background-color: rgba(247, 248, 250, 1);
border: 1px solid #fff;
border-radius: 16rpx;
display: flex;
.icon-wrap {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
.icon {
width: 26rpx;
height: 21rpx;
}
.icon:last-of-type {
display: none;
}
&.active {
background-color: #fff;
.icon {
display: none;
}
.icon:last-of-type {
display: block;
}
}
}
}
}
.calender {
margin-top: 30rpx;
padding: 16rpx;
background: #ffffff;
box-shadow: 0rpx 15rpx 30rpx 0rpx rgba(74, 172, 219, 0.09);
border-radius: 24rpx 24rpx 24rpx 24rpx;
border: 1rpx solid #f7f8fa;
display: flex;
align-content: inherit;
.item {
flex: 1;
padding: 16rpx;
text-align: center;
border-radius: 24rpx;
.week-name {
font-size: 18rpx;
color: rgba(148, 163, 184, 1);
}
.day {
margin-top: 8rpx;
font-size: 28rpx;
color: rgba(17, 24, 39, 1);
line-height: 38rpx;
}
&.active {
background-color: rgba(74, 184, 253, 1);
box-shadow:
0rpx 8rpx 12rpx -8rpx rgba(74, 184, 253, 0.2),
0rpx 19rpx 29rpx -6rpx rgba(74, 184, 253, 0.2);
.week-name {
color: #fff;
}
.day {
color: #fff;
}
}
}
}
.format1 {
margin-top: 18rpx;
.card {
margin-top: 30rpx;
display: flex;
gap: 30rpx;
.aside {
flex-shrink: 0;
padding-top: 15rpx;
display: flex;
flex-direction: column;
align-items: flex-end;
.start {
font-size: 28rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.line {
flex-shrink: 0;
width: 1px;
height: 29rpx;
background-color: rgba(148, 163, 184, 1);
}
.end {
font-size: 24rpx;
color: rgba(148, 163, 184, 1);
}
}
.container {
flex: 1;
padding: 33rpx;
border-radius: 24rpx;
border-left: 10rpx solid rgba(171, 89, 248, 1);
background-color: rgba(171, 89, 248, 0.1);
.c-header {
display: flex;
align-items: center;
.title {
font-size: 32rpx;
color: rgba(74, 21, 124, 1);
font-weight: bold;
.step {
padding: 6rpx 16rpx;
font-size: 22rpx;
color: rgba(103, 31, 171, 1);
background-color: rgba(171, 89, 248, 0.2);
border-radius: 242rpx;
}
}
}
.site,
.teacher {
padding-top: 16rpx;
display: flex;
gap: 12rpx;
.icon {
margin-top: 14rpx;
}
.content {
color: rgba(103, 31, 171, 1);
}
}
}
}
}
.format2 {
}
.format3 {
/* 外层容器 */
.schedule-wrap {
border: 1rpx solid #eee;
border-radius: 16rpx;
overflow: hidden;
}
/* 表头横向滚动 */
.header-scroll {
width: 100%;
}
.header-row {
display: flex;
}
/* 左侧时间列统一宽度 */
.time-col {
width: 160rpx;
flex-shrink: 0;
}
.header-time {
display: flex;
align-items: center;
justify-content: center;
border-right: 1rpx solid #eee;
border-bottom: 1rpx solid #eee;
color: #999;
}
/* 每一天的列宽统一 */
.day-col {
width: 120rpx;
flex-shrink: 0;
}
.header-day {
text-align: center;
padding: 16rpx 0;
border-right: 1rpx solid #eee;
border-bottom: 1rpx solid #eee;
}
.header-day .week {
font-size: 26rpx;
color: #666;
}
.header-day .date {
font-size: 30rpx;
margin-top: 4rpx;
}
/* 主体横向+纵向滚动 */
.body-scroll {
height: calc(100vh - 160rpx);
}
.body-row {
display: flex;
}
/* 左侧课时列表 */
.body-time {
border-right: 1rpx solid #eee;
}
.time-item {
height: 140rpx; /* 单节高度,和课程网格行高一致 */
padding: 10rpx;
border-bottom: 1rpx solid #eee;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #666;
font-size: 24rpx;
}
.section-name {
font-weight: 500;
}
.time-range {
font-size: 22rpx;
color: #999;
margin-top: 6rpx;
}
/* 课程网格核心:CSS Grid */
.grid-container {
display: flex;
}
.grid-day {
display: grid;
grid-template-rows: repeat(9, 140rpx); /* 9节课,每行140rpx */
border-right: 1rpx solid #eee;
position: relative;
}
/* 课程卡片样式 */
.course-card {
margin: 8rpx;
border-radius: 8rpx;
padding: 10rpx;
color: #333;
font-size: 24rpx;
}
.course-name {
font-weight: 500;
line-height: 1.4;
}
.course-loc {
margin-top: 8rpx;
font-size: 22rpx;
opacity: 0.8;
}
}
}

149
src/pages/schedule/index.ts

@ -0,0 +1,149 @@
const _app = getApp<IAppOption>()
Page({
data: {
scrollX: 0, // 横向滚动偏移量
isSyncing: false, // 同步锁,防止双向滚动死循环
todayIndex: 1, // 今天对应的索引(周二=1)
// 左侧9节课时间段
sectionList: [
{ index: 1, time: '08:30\n09:15' },
{ index: 2, time: '09:20\n10:05' },
{ index: 3, time: '10:25\n11:10' },
{ index: 4, time: '11:15\n12:00' },
{ index: 5, time: '14:00\n14:45' },
{ index: 6, time: '14:50\n15:35' },
{ index: 7, time: '15:45\n16:30' },
{ index: 8, time: '16:35\n17:20' },
{ index: 9, time: '17:30\n18:10' },
],
// 周一到周日 日期+课程数据
weekList: [
{
week: '一',
date: '6/1',
courseList: [
{
id: 1,
name: '公共外语II(英语综合)',
loc: '日新楼北401',
start: 1,
rowNum: 2,
bgColor: '#e6f7ff',
borderColor: '#4ab8fd',
textColor: '#4ab8fd',
locColor: '#4ab8fd',
},
],
},
{
week: '二',
date: '6/2',
courseList: [
{
id: 2,
name: '数据库SQL入门',
loc: '日新楼北401',
start: 3,
rowNum: 2,
bgColor: '#fff0f0',
borderColor: '#ff6b6b',
textColor: '#ff6b6b',
locColor: '#ff6b6b',
},
{
id: 3,
name: 'BIM建筑信息模型',
loc: '信息楼507机房',
start: 5,
rowNum: 2,
bgColor: '#fff8e6',
borderColor: '#ffa500',
textColor: '#ffa500',
locColor: '#ffa500',
},
],
},
{ week: '三', date: '6/3', courseList: [] },
{
week: '四',
date: '6/4',
courseList: [
{
id: 4,
name: '(美育课)人工智能伦理与哲学',
loc: '日新楼北301',
start: 3,
rowNum: 2,
bgColor: '#e6fff0',
borderColor: '#52c41a',
textColor: '#52c41a',
locColor: '#52c41a',
},
],
},
{ week: '五', date: '6/5', courseList: [] },
{
week: '六',
date: '6/6',
courseList: [
{
id: 5,
name: '体育与健康2(健美操)',
loc: 'D6体育馆三',
start: 7,
rowNum: 2,
bgColor: '#e6f0ff',
borderColor: '#4ab8fd',
textColor: '#4ab8fd',
locColor: '#4ab8fd',
},
],
},
{
week: '日',
date: '6/7',
courseList: [
{
id: 6,
name: '创新思维',
loc: '日新楼北301',
start: 1,
rowNum: 2,
bgColor: '#f5e6ff',
borderColor: '#b37feb',
textColor: '#b37feb',
locColor: '#b37feb',
},
],
},
],
},
onLoad() {},
// 滑动表头时,同步主体滚动
onHeaderScroll(e) {
if (this.data.isSyncing) return
this.setData({
isSyncing: true,
scrollX: e.detail.scrollLeft,
})
// 延迟解锁同步锁,避免双向触发循环
setTimeout(() => {
this.setData({ isSyncing: false })
}, 80)
},
// 滑动课程主体时,同步表头滚动
onBodyScroll(e) {
if (this.data.isSyncing) return
this.setData({
isSyncing: true,
scrollX: e.detail.scrollLeft,
})
setTimeout(() => {
this.setData({ isSyncing: false })
}, 80)
},
})
export {}

142
src/pages/schedule/index.wxml

@ -0,0 +1,142 @@
<navbar fixed customStyle="background:{{background}};">
<van-icon class="page-back" name="arrow-left" slot="left" />
<view class="page-title" slot="title">SIC+ 课表</view>
</navbar>
<view class="page" style="padding-top: {{pageTop}}px;">
<view class="page-header">
<view class="week-wrap">
<view class="week">
<image class="icon" src="/images/icon69.png"></image>
<view class="content">第14周</view>
<image class="icon" src="/images/icon70.png"></image>
</view>
<view class="school-year">2025-2026 第1学期</view>
</view>
<view class="notify">
<view class="icon-wrap">
<image class="icon" src="/images/icon71.png"></image>
</view>
提醒我
</view>
<view class="switch-format">
<view class="icon-wrap active">
<image class="icon" src="/images/icon74.png"></image>
<image class="icon" src="/images/icon72.png"></image>
</view>
<view class="icon-wrap">
<image class="icon" src="/images/icon73.png"></image>
<image class="icon" src="/images/icon75.png"></image>
</view>
</view>
</view>
<view class="calender">
<view class="item {{index==2 && 'active'}}" wx:for="{{7}}" wx:key="index">
<view class="week-name">一</view>
<view class="day">6/1</view>
</view>
</view>
<view class="format1" wx:if="{{false}}">
<view class="card" wx:for="{{20}}" wx:key="index">
<view class="aside">
<view class="start">08:00</view>
<view class="line"></view>
<view class="end">09:35</view>
</view>
<view class="container">
<view class="c-header">
<view class="title">微积分 (Calculus)</view>
<view class="step">第1-2节</view>
</view>
<view class="site">
<svg-icon class="icon" width="18rpx" height="18rpx" src="/images/svg1.svg"></svg-icon>
<view class="content">博学楼4教室</view>
</view>
<view class="teacher">
<svg-icon class="icon" width="18rpx" height="18rpx" src="/images/svg2.svg"></svg-icon>
<view class="content">李老师</view>
</view>
</view>
</view>
</view>
<view class="format2" wx:else>
<!-- 顶部日期表头 -->
<view class="schedule-header">
<view class="header-time">时间</view>
<view class="header-days">
<view wx:for="{{weekList}}" wx:key="date" class="day-header {{index === todayIndex ? 'active' : ''}}">
<view class="day-week">{{item.week}}</view>
<view class="day-date">{{item.date}}</view>
</view>
</view>
</view>
<!-- 课程主体区域 -->
<scroll-view scroll-x class="schedule-body">
<view class="body-row">
<!-- 左侧节次列 -->
<view class="body-time">
<view wx:for="{{sectionList}}" wx:key="index" class="time-slot">
<view class="slot-name">第{{item.index}}节</view>
<view class="slot-range">{{item.time}}</view>
</view>
</view>
<!-- 右侧课程网格 -->
<view class="body-grid">
<view wx:for="{{weekList}}" wx:key="date" class="day-column">
<view
wx:for="{{item.courseList}}"
wx:key="id"
class="course-block"
style="background-color: {{item.bgColor}}; border-color: {{item.borderColor}}; top: {{(item.start - 1) * 140}}rpx; height: {{item.rowNum * 140 - 2}}rpx;"
>
<view class="course-title" style="color: {{item.textColor}}">{{item.name}}</view>
<view class="course-loc" style="color: {{item.locColor}}">{{item.loc}}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="format3">
<view class="schedule-wrap">
<!-- 顶部表头:绑定滚动事件 -->
<scroll-view scroll-x class="header-scroll" bindscroll="onHeaderScroll" scroll-left="{{scrollX}}">
<view class="header-row">
<view class="time-col header-time">时间</view>
<view wx:for="{{weekList}}" wx:key="date" class="day-col header-day">
<view class="week">{{item.week}}</view>
<view class="date">{{item.date}}</view>
</view>
</view>
</scroll-view>
<!-- 主体内容:绑定滚动事件,同步横向偏移 -->
<scroll-view scroll-x scroll-y class="body-scroll" bindscroll="onBodyScroll" scroll-left="{{scrollX}}">
<view class="body-row">
<view class="time-col body-time">
<view wx:for="{{sectionList}}" wx:key="index" class="time-item">
<view class="section-name">第{{item.index}}节</view>
<view class="time-range">{{item.time}}</view>
</view>
</view>
<view class="grid-container">
<view wx:for="{{weekList}}" wx:key="date" class="day-col grid-day">
<view
wx:for="{{item.courseList}}"
wx:key="id"
class="course-card"
style="background:{{item.bgColor}}; grid-row: {{item.start}} / span {{item.rowNum}}"
>
<view class="course-name">{{item.name}}</view>
<view class="course-loc">{{item.loc}}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>

29
typings/index.d.ts vendored

@ -1,3 +1,22 @@
/**
*
*/
interface UserInfo {
id: number
nickname?: string
avatarUrl?: string
realName?: string
studentNo?: string
role?: 'student' | 'teacher' | 'staff'
collegeId?: number
collegeName?: string
majorId?: number
majorName?: string
classId?: number
className?: string
grade?: string
}
interface IAppOption { interface IAppOption {
globalData: { globalData: {
url?: string url?: string
@ -7,13 +26,21 @@ interface IAppOption {
Timestamp: number Timestamp: number
waitBindDoctorId: string waitBindDoctorId: string
loginState: string
initLoginInfo: Partial<{ initLoginInfo: Partial<{
isLogin: 0 | 1 isLogin: 0 | 1
isReg: 0 | 1 isReg: 0 | 1
loginType: 1 | 2 loginType: 1 | 2
needBind?: boolean
user?: UserInfo
}> }>
// JWT 令牌相关
accessToken?: string
tokenExpireIn?: number
openidSession?: string // 临时凭证(用于绑定)
userInfo?: UserInfo
loginRedirectUrl?: string // 登录后返回的页面路径
[propName: string]: any [propName: string]: any
} }
getUserInfo: (type?: 0 | 1 | 2) => Promise<any> getUserInfo: (type?: 0 | 1 | 2) => Promise<any>

Loading…
Cancel
Save