diff --git a/README.md b/README.md index a41ed9d..7d80a85 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,10 @@ powershell 软链形式 ``` New-Item -ItemType Junction -Path "src/images" -Target C:\Users\kola\project\school-system\web_dist\images ``` + +测试账号 + +``` +账号 2026050122123 +密码 123456 +``` diff --git a/project.private.config.json b/project.private.config.json index 3a43ce0..81dd7a9 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -4,13 +4,20 @@ "miniprogram": { "list": [ { - "name": "登录", - "pathName": "pages/login/index", + "name": "课表", + "pathName": "pages/schedule/index", "query": "", "scene": null, "launchMode": "default" }, { + "name": "登录", + "pathName": "pages/login/index", + "query": "", + "launchMode": "default", + "scene": null + }, + { "name": "我的评论", "pathName": "pages/myComment/index", "query": "", diff --git a/src/api/request.ts b/src/api/request.ts index f0d3379..8251f2c 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -24,15 +24,16 @@ export const request = function ( mask: true, }) } + + const app = getApp() + wx.request({ header: { - loginState: getApp().globalData.loginState, ...header, }, url: gUrl + url, method, data: { - loginState: getApp().globalData.loginState, ...(data as object), }, ...options, diff --git a/src/app.json b/src/app.json index 3278354..5fc9f82 100644 --- a/src/app.json +++ b/src/app.json @@ -18,7 +18,8 @@ "pages/agentEva/index", "pages/myAct/index", "pages/myAgent/index", - "pages/myComment/index" + "pages/myComment/index", + "pages/schedule/index" ], "window": { "backgroundTextStyle": "light", diff --git a/src/app.ts b/src/app.ts index 4f656b1..236f6f4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,9 +1,9 @@ +/* eslint-disable perfectionist/sort-imports */ +import dayjs from 'dayjs' import page from '@/utils/page' - import { request } from './api/request' - import { parseScene } from './utils/util' -const dayjs = require('dayjs') + const licia = require('miniprogram-licia') require('/utils/dayjs/day-zh-cn.js') const relativeTime = require('/utils/dayjs/relativeTime.js') @@ -12,8 +12,8 @@ dayjs.extend(relativeTime) App({ globalData: { - url: '', - upFileUrl: '', + url: 'https://app.gohighedu.cn', + upFileUrl: 'https://app.gohighedu.cn', imageUrl: 'https://app.gohighedu.cn/images', Timestamp: new Date().getTime(), @@ -21,9 +21,7 @@ App({ waitBindDoctorId: '', scene: {}, - loginState: '', initLoginInfo: {}, - userInfo: {}, }, onLaunch() { @@ -42,19 +40,44 @@ App({ startLogin(callback?: () => void) { wx.login({ success: (res) => { - wx.ajax({ - method: 'GET', - url: '?r=wtx/user/init-login', - data: { - code: res.code, - }, - }).then((res: any) => { - this.globalData.loginState = res.loginState - this.globalData.initLoginInfo = res - if (callback) { - callback() - } - }) + console.log("DEBUGPRINT[244]: app.ts:42: res=", res) + // // 调用静默登录接口 + // wx.ajax({ + // method: 'POST', + // 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) + // }) + }, + fail: (err) => { + // 静默失败,不提示用户 + console.error('wx.login 失败:', err) }, }) }, @@ -71,54 +94,92 @@ App({ }) }, waitLogin({ type }: { type?: 0 | 1 | 2 | 'any' } = { type: 'any' }) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const checkLogin = () => { - if (this.globalData.loginState) { - if (this.checkLoginType(type ?? 'any')) { + // type = 0:不需要登录即可访问 + if (type === 0) { + resolve() + return + } + + // type = 'any':不检查登录状态 + if (type === 'any') { + resolve() + return + } + + // type = 1 或 2:需要登录 + 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 } - setTimeout(() => { - checkLogin() - }, 500) + + resolve() } checkLogin() }) }, + + /** + * 重定向到登录页,并记录当前页面路径 + */ + redirectToLogin(_type: 1 | 2) { + // 获取当前页面路径 + const pages = getCurrentPages() + const currentPage = pages[pages.length - 1] + const currentUrl = currentPage ? currentPage.route : '' + + // 记录来源页面,登录后返回 + if (currentUrl && currentUrl !== 'pages/login/index') { + this.globalData.loginRedirectUrl = currentUrl + } + + wx.reLaunch({ + url: '/pages/login/index', + }) + }, + checkLoginType(type: 0 | 1 | 2 | 'any') { - const { loginType, isLogin, isReg } = this.globalData.initLoginInfo + // type = 0:不需要登录 + if (type === 0) { + return true + } + // type = 'any':不检查 if (type === 'any') { return true } - if (isLogin !== 1) { - if (type === 0) { - return true + // type = 1 或 2:需要登录 + if (type === 1 || type === 2) { + // 检查是否有 accessToken + if (!this.globalData.accessToken) { + this.redirectToLogin(type as 1 | 2) + return false } - wx.reLaunch({ - url: '/pages/index/index', - }) - return false - } - if (isReg !== 1) { - const typePageUrl: Record = { - 1: '/pages/login/index', - 2: '/pages/login/index', + // 检查是否需要绑定 + if (this.globalData.initLoginInfo?.needBind) { + this.redirectToLogin(type as 1 | 2) + return false } - wx.reLaunch({ - url: typePageUrl[loginType as 1 | 2], - }) - return false - } - if (loginType !== type) { - wx.reLaunch({ - url: '/pages/index/index', - }) - return false + return true } return true @@ -135,7 +196,6 @@ App({ }) }, autoUpdate() { - const self = this if (wx.canIUse('getUpdateManager')) { const updateManager = wx.getUpdateManager() updateManager.onCheckForUpdate((res) => { @@ -143,18 +203,18 @@ App({ wx.showModal({ title: '更新提示', content: '检测到新版本,是否下载新版本并重启小程序?', - success(res) { + success: (res) => { if (res.confirm) { - self.downLoadAndUpdate(updateManager) + this.downLoadAndUpdate(updateManager) } else if (res.cancel) { wx.showModal({ title: '温馨提示~', content: '本次版本更新涉及到新的功能添加,旧版本无法正常访问的哦~', showCancel: false, confirmText: '确定更新', - success(res) { + success: (res) => { if (res.confirm) { - self.downLoadAndUpdate(updateManager) + this.downLoadAndUpdate(updateManager) } }, }) @@ -165,8 +225,8 @@ App({ }) } else { wx.showModal({ - title: '提示', - content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。', + title: '温馨提示', + content: '当前微信版本过低,无法使用版本更新功能,请升级到最新微信版本后重试。', }) } }, diff --git a/src/components/svg-icon/README.md b/src/components/svg-icon/README.md index a41ac4b..4cb7ac8 100644 --- a/src/components/svg-icon/README.md +++ b/src/components/svg-icon/README.md @@ -26,96 +26,32 @@ + + + + ``` -## 单颜色重新着色 +## 重新着色 传入 `color` 时,会对 SVG 中所有声明 `fill/stroke` 的元素统一着色: ```xml + ``` -## 多颜色重新着色 - -### 数组形式(按顺序替换) - -以数组形式传入 `colors` 时,依照数组中的颜色顺序,对 SVG 中所有声明 `fill/stroke` 的元素按顺序重新着色: - -```ts -// page.ts -Page({ - data: { - colorsArray: ['#ff0000', '#00ff00', '#0000ff'], - }, -}) -``` - -```xml - - -``` - -### 对象形式(按键值关系替换) - -以对象形式传入 `colors` 时,依照对象中的键值关系,对 SVG 中所有声明 `fill/stroke` 的元素按对应关系重新着色: - -```ts -// page.ts -Page({ - data: { - colorsObject: { - black: '#ff0000', - '#fff': '#00ff00', - '#808080': '#cdcdcd', - }, - }, -}) -``` - -```xml - - -``` - -## 组合重新着色 - -同时传入 `color` 和 `colors` 组合搭配,既能为指定元素重新着色,也能为其余未指定元素统一着色: - -```ts -// page.ts -Page({ - data: { - colorsArray: ['#ff0000', '#00ff00'], - }, -}) -``` - -```xml - - -``` - -## 网络资源 - -支持传入网络 SVG 资源地址: - -```xml - -``` - -**注意**:当 src 传入网络资源并重新着色时,请将网络资源的域名配置于小程序的 `downloadFile` 合法域名中。 - ## API ### Properties | 参数 | 说明 | 类型 | 默认值 | 必填 | | --- | --- | --- | --- | --- | -| `src` | SVG 资源地址(支持本地路径、临时路径、网络资源) | `string` | `''` | 是 | -| `color` | SVG 单一颜色(对所有 fill/stroke 元素统一着色) | `string` | `''` | 否 | -| `colors` | SVG 多颜色配置(支持数组或对象) | `array \| object` | `null` | 否 | +| `src` | SVG 资源地址(支持本地路径) | `string` | `''` | 是 | +| `color` | SVG 颜色(对所有 fill/stroke 元素统一着色) | `string` | `''` | 否 | | `mode` | SVG 裁剪、缩放模式(与 `image` 标签相同) | `string` | `''` | 否 | +| `width` | SVG 宽度(支持 rpx/px 单位) | `string` | `'48rpx'` | 否 | +| `height` | SVG 高度(支持 rpx/px 单位) | `string` | `'48rpx'` | 否 | ### Events @@ -153,12 +89,15 @@ Page({ ## 实现原理 -1. 读取 SVG 文件内容(本地文件或网络文件) +1. 读取 SVG 文件内容 2. 通过正则替换 SVG 中的 `fill/stroke` 属性值来实现改色 3. 将修改后的 SVG 内容转为 base64 格式,作为 `image` 的 `src` ## 注意事项 - SVG 文件必须包含 `fill` 或 `stroke` 属性才能被改色 +- 支持本地路径、网络地址(HTTP/HTTPS)、临时路径(wxfile://) +- **开发工具限制**:由于开发工具的文件系统权限限制,本地路径的 SVG 可能无法改色,此时会直接显示原 SVG(不改色) +- **正式环境建议**:使用网络地址,这样可以正常改色 - 网络资源需要配置 `downloadFile` 合法域名 -- 组件会缓存下载的网络资源,避免重复下载 \ No newline at end of file +- 组件会缓存下载的网络资源,避免重复下载 diff --git a/src/components/svg-icon/index.ts b/src/components/svg-icon/index.ts index a86ea35..08c478d 100644 --- a/src/components/svg-icon/index.ts +++ b/src/components/svg-icon/index.ts @@ -7,22 +7,90 @@ import { encode } from './base64' const fs = wx.getFileSystemManager() -// 临时文件缓存(网络资源下载后缓存) -const tempFileMap = new Map() +// 网络资源缓存(避免重复下载) +const networkCache = new Map() /** - * 同步下载网络文件 - * @param url 网络资源地址 - * @returns 下载结果 + * 判断是否为网络资源 */ -function downloadFileSync(url: string): Promise { - return new Promise((resolve, reject) => { +function isNetworkUrl(src: string): boolean { + 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 { + // 检查缓存 + const cachedPath = networkCache.get(url) + if (cachedPath) { + try { + fs.accessSync(cachedPath) + return cachedPath + } catch { + // 缓存失效,重新下载 + networkCache.delete(url) + } + } + + // 下载文件 + const downloadResult = await new Promise((resolve, reject) => { wx.downloadFile({ url, success: resolve, fail: reject, }) }) + + // 缓存临时文件路径 + networkCache.set(url, downloadResult.tempFilePath) + + return downloadResult.tempFilePath } Component({ @@ -33,100 +101,102 @@ Component({ externalClasses: ['image-class'], properties: { - /** SVG 资源地址(支持本地路径、临时路径、网络资源) */ + /** SVG 资源地址(支持本地路径、网络地址、临时路径) */ src: { type: String, value: '', }, - /** SVG 单一颜色(对所有 fill/stroke 元素统一着色) */ + /** SVG 颜色(对所有 fill/stroke 元素统一着色) */ color: { type: String, value: '', }, - /** SVG 多颜色配置(支持数组或对象) */ - colors: { - type: null, - value: null, - }, /** SVG 裁剪、缩放模式(与 image 标签相同) */ mode: { type: String, value: '', }, + /** SVG 宽度(支持 rpx/px 单位,默认 48rpx) */ + width: { + type: String, + value: '48rpx', + }, + /** SVG 高度(支持 rpx/px 单位,默认 48rpx) */ + height: { + type: String, + value: '48rpx', + }, }, observers: { - 'src, color, colors': async function (src: string, color: string, colors: unknown) { + 'src, color': async function (src: string, color: string) { try { - // 如果需要改色 - if (color || (colors && ((Array.isArray(colors) && colors.length > 0) || (typeof colors === 'object' && Object.keys(colors).length > 0)))) { - let svgData: string - - // 判断是否为网络资源(排除开发工具临时路径 http://tmp/) - if (/^https?:\/\//.test(src) && !/^http:\/\/tmp\//.test(src)) { - // 网络资源需要先下载 - let tempFilePath = tempFileMap.get(src) - try { - if (!tempFilePath) throw new Error('未缓存') - // 检查临时文件是否存在 - fs.accessSync(tempFilePath) - } catch { - // 下载文件 - const downloadResult = await downloadFileSync(src) - tempFilePath = downloadResult.tempFilePath - // 缓存临时文件路径 - tempFileMap.set(src, tempFilePath) - } - // 读取文件内容 - svgData = fs.readFileSync(tempFilePath, 'utf8') as string - } else { - // 本地资源直接读取 - svgData = fs.readFileSync(src, 'utf8') as string - } + if (!src) { + this.setData({ base64: '' }) + 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 - } - - // 返回替换后的属性 - if (/fill/.test(matched)) return `fill="${replaceColor}"` - if (/stroke/.test(matched)) return `stroke="${replaceColor}"` - return `fill="${replaceColor}"` - }) - } + // 如果不需要改色,直接使用原路径 + if (!color) { + this.setData({ base64: src }) + return + } - // 设置默认底色(SVG 根元素) - const defaultColor = (typeof colorsConfig === 'object' && !Array.isArray(colorsConfig) && (colorsConfig['#000'] || colorsConfig['#000000'] || colorsConfig.black)) || color - if (defaultColor && !/fill=".*?"/.test(svgData.slice(0, svgData.indexOf('>')))) { - svgData = svgData.replace(/ { + if (/fill/.test(matched)) return `fill="${color}"` + if (/stroke/.test(matched)) return `stroke="${color}"` + return `fill="${color}"` }) - } else { - // 不需要改色,直接使用原路径 - this.setData({ base64: src }) } + + // 设置 SVG 根元素的默认底色 + if (!/fill="[^"]*"/.test(modifiedSvg.slice(0, modifiedSvg.indexOf('>')))) { + modifiedSvg = modifiedSvg.replace(/ \ No newline at end of file +/> diff --git a/src/images/icon69.png b/src/images/icon69.png new file mode 100644 index 0000000..17d41d3 Binary files /dev/null and b/src/images/icon69.png differ diff --git a/src/images/icon70.png b/src/images/icon70.png new file mode 100644 index 0000000..aa4fa02 Binary files /dev/null and b/src/images/icon70.png differ diff --git a/src/images/icon71.png b/src/images/icon71.png new file mode 100644 index 0000000..1ffc09e Binary files /dev/null and b/src/images/icon71.png differ diff --git a/src/images/icon72.png b/src/images/icon72.png new file mode 100644 index 0000000..bae13c0 Binary files /dev/null and b/src/images/icon72.png differ diff --git a/src/images/icon73.png b/src/images/icon73.png new file mode 100644 index 0000000..92299a5 Binary files /dev/null and b/src/images/icon73.png differ diff --git a/src/images/icon74.png b/src/images/icon74.png new file mode 100644 index 0000000..1e8e433 Binary files /dev/null and b/src/images/icon74.png differ diff --git a/src/images/icon75.png b/src/images/icon75.png new file mode 100644 index 0000000..d7b99b7 Binary files /dev/null and b/src/images/icon75.png differ diff --git a/src/images/svg2.svg b/src/images/svg2.svg new file mode 100644 index 0000000..073c448 --- /dev/null +++ b/src/images/svg2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/pages/login/index.json b/src/pages/login/index.json index a97367d..40df7ae 100644 --- a/src/pages/login/index.json +++ b/src/pages/login/index.json @@ -1,3 +1,4 @@ { + "navigationBarTitleText": "登录", "usingComponents": {} } diff --git a/src/pages/login/index.scss b/src/pages/login/index.scss index d440f29..97d0e5d 100644 --- a/src/pages/login/index.scss +++ b/src/pages/login/index.scss @@ -53,6 +53,7 @@ } } .input { + flex: 1; height: 84rpx; font-size: 32rpx; color: rgba(71, 85, 105, 1); @@ -61,18 +62,37 @@ color: rgba(203, 213, 225, 1); } } - .tip2{ + .tip2 { margin-top: 28rpx; font-size: 28rpx; color: rgba(148, 163, 184, 1); line-height: 42rpx; } - .agreement{ + .agreement { + margin-top: 42rpx; font-size: 28rpx; 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); } } + .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; + } } } diff --git a/src/pages/login/index.ts b/src/pages/login/index.ts index ded2028..fbf1963 100644 --- a/src/pages/login/index.ts +++ b/src/pages/login/index.ts @@ -1,8 +1,152 @@ const app = getApp() Page({ - data: {}, - onLoad() {}, + data: { + 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() { wx.getUserProfile({ desc: '用于完善用户资料', diff --git a/src/pages/login/index.wxml b/src/pages/login/index.wxml index fe35598..a88f0e7 100644 --- a/src/pages/login/index.wxml +++ b/src/pages/login/index.wxml @@ -16,15 +16,33 @@ 账号 - + 密码 - + SIC+仅将您的密码用于单次身份验证,不会保存您的密码 - 我已阅读并接受《深职大SIC+小程序隐私保护指引》 + + 我已阅读并接受 + 《深职大SIC+小程序隐私保护指引》 + + 提交 diff --git a/src/pages/schedule/index.json b/src/pages/schedule/index.json new file mode 100644 index 0000000..52e5e1d --- /dev/null +++ b/src/pages/schedule/index.json @@ -0,0 +1,7 @@ +{ + "navigationBarTitleText": "课表", + "usingComponents": { + "van-icon": "@vant/weapp/icon/index", + "svg-icon": "/components/svg-icon/index" + } +} diff --git a/src/pages/schedule/index.scss b/src/pages/schedule/index.scss new file mode 100644 index 0000000..a34ae29 --- /dev/null +++ b/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; + } + } +} diff --git a/src/pages/schedule/index.ts b/src/pages/schedule/index.ts new file mode 100644 index 0000000..89d1a56 --- /dev/null +++ b/src/pages/schedule/index.ts @@ -0,0 +1,149 @@ +const _app = getApp() + +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 {} diff --git a/src/pages/schedule/index.wxml b/src/pages/schedule/index.wxml new file mode 100644 index 0000000..a418e39 --- /dev/null +++ b/src/pages/schedule/index.wxml @@ -0,0 +1,142 @@ + + + SIC+ 课表 + + + + + + + 第14周 + + + 2025-2026 第1学期 + + + + + + 提醒我 + + + + + + + + + + + + + + + + 6/1 + + + + + + + 08:00 + + 09:35 + + + + 微积分 (Calculus) + 第1-2节 + + + + 博学楼4教室 + + + + 李老师 + + + + + + + + + 时间 + + + {{item.week}} + {{item.date}} + + + + + + + + + + + 第{{item.index}}节 + {{item.time}} + + + + + + + {{item.name}} + {{item.loc}} + + + + + + + + + + + + + 时间 + + {{item.week}} + {{item.date}} + + + + + + + + + + 第{{item.index}}节 + {{item.time}} + + + + + + {{item.name}} + {{item.loc}} + + + + + + + + diff --git a/typings/index.d.ts b/typings/index.d.ts index fb2a2b0..ee96425 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -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 { globalData: { url?: string @@ -7,13 +26,21 @@ interface IAppOption { Timestamp: number waitBindDoctorId: string - loginState: string initLoginInfo: Partial<{ isLogin: 0 | 1 isReg: 0 | 1 loginType: 1 | 2 + needBind?: boolean + user?: UserInfo }> + // JWT 令牌相关 + accessToken?: string + tokenExpireIn?: number + openidSession?: string // 临时凭证(用于绑定) + userInfo?: UserInfo + loginRedirectUrl?: string // 登录后返回的页面路径 + [propName: string]: any } getUserInfo: (type?: 0 | 1 | 2) => Promise