Browse Source

feat: 新增大量页面、组件与资源,完善校园小程序功能

1. 新增校园巴士、智能体、我的活动等多个业务页面
2. 新增svg-icon、uploadFile通用组件
3. 新增数十个图标与背景图片资源
4. 优化活动创建、详情页面的上传与样式逻辑
5. 更新全局配置与自定义tabbar
master
kola-web 2 weeks ago
parent
commit
7b4e691193
  1. 8
      README.md
  2. 12
      dist.ps1
  3. 74
      project.private.config.json
  4. 14
      src/app.json
  5. 6
      src/app.ts
  6. 164
      src/components/svg-icon/README.md
  7. 34
      src/components/svg-icon/base64.ts
  8. 4
      src/components/svg-icon/index.json
  9. 7
      src/components/svg-icon/index.scss
  10. 156
      src/components/svg-icon/index.ts
  11. 11
      src/components/svg-icon/index.wxml
  12. 158
      src/components/uploadFile/README.md
  13. 4
      src/components/uploadFile/index.json
  14. 272
      src/components/uploadFile/index.scss
  15. 363
      src/components/uploadFile/index.ts
  16. 92
      src/components/uploadFile/index.wxml
  17. 2
      src/custom-tab-bar/index.ts
  18. BIN
      src/images/bg3.png
  19. BIN
      src/images/bg4.png
  20. BIN
      src/images/bg5.png
  21. BIN
      src/images/bg6.png
  22. BIN
      src/images/gif1.gif
  23. BIN
      src/images/icon43.png
  24. BIN
      src/images/icon44.png
  25. BIN
      src/images/icon45.png
  26. BIN
      src/images/icon46.png
  27. BIN
      src/images/icon47.png
  28. BIN
      src/images/icon48.png
  29. BIN
      src/images/icon49.png
  30. BIN
      src/images/icon50.png
  31. BIN
      src/images/icon51.png
  32. BIN
      src/images/icon52.png
  33. BIN
      src/images/icon53.png
  34. BIN
      src/images/icon54.png
  35. BIN
      src/images/icon55.png
  36. BIN
      src/images/icon56.png
  37. BIN
      src/images/icon57.png
  38. BIN
      src/images/icon58.png
  39. BIN
      src/images/icon59.png
  40. BIN
      src/images/icon60.png
  41. BIN
      src/images/icon61.png
  42. BIN
      src/images/icon62.png
  43. BIN
      src/images/icon63.png
  44. BIN
      src/images/icon64.png
  45. BIN
      src/images/icon65.png
  46. BIN
      src/images/icon66.png
  47. BIN
      src/images/icon67.png
  48. BIN
      src/images/icon68.png
  49. 12
      src/images/svg1.svg
  50. 4
      src/pages/act/index.scss
  51. 4
      src/pages/actAdd/index.json
  52. 16
      src/pages/actAdd/index.scss
  53. 17
      src/pages/actAdd/index.ts
  54. 16
      src/pages/actAdd/index.wxml
  55. 4
      src/pages/actDetail/index.json
  56. 122
      src/pages/actDetail/index.scss
  57. 7
      src/pages/actDetail/index.ts
  58. 32
      src/pages/actDetail/index.wxml
  59. 9
      src/pages/agent/index.json
  60. 250
      src/pages/agent/index.scss
  61. 13
      src/pages/agent/index.ts
  62. 112
      src/pages/agent/index.wxml
  63. 7
      src/pages/agentEva/index.json
  64. 219
      src/pages/agentEva/index.scss
  65. 8
      src/pages/agentEva/index.ts
  66. 77
      src/pages/agentEva/index.wxml
  67. 7
      src/pages/buses/index.json
  68. 314
      src/pages/buses/index.scss
  69. 53
      src/pages/buses/index.ts
  70. 83
      src/pages/buses/index.wxml
  71. 7
      src/pages/chat/index.json
  72. 160
      src/pages/chat/index.scss
  73. 251
      src/pages/chat/index.ts
  74. 69
      src/pages/chat/index.wxml
  75. 5
      src/pages/chatHistory/index.json
  76. 29
      src/pages/chatHistory/index.scss
  77. 8
      src/pages/chatHistory/index.ts
  78. 9
      src/pages/chatHistory/index.wxml
  79. 1
      src/pages/index/index.wxml
  80. 77
      src/pages/login/index.scss
  81. 31
      src/pages/login/index.wxml
  82. 6
      src/pages/my/index.json
  83. 95
      src/pages/my/index.scss
  84. 15
      src/pages/my/index.ts
  85. 49
      src/pages/my/index.wxml
  86. 9
      src/pages/myAct/index.json
  87. 141
      src/pages/myAct/index.scss
  88. 8
      src/pages/myAct/index.ts
  89. 49
      src/pages/myAct/index.wxml
  90. 9
      src/pages/myAgent/index.json
  91. 95
      src/pages/myAgent/index.scss
  92. 8
      src/pages/myAgent/index.ts
  93. 31
      src/pages/myAgent/index.wxml
  94. 10
      src/pages/myComment/index.json
  95. 167
      src/pages/myComment/index.scss
  96. 8
      src/pages/myComment/index.ts
  97. 58
      src/pages/myComment/index.wxml

8
README.md

@ -0,0 +1,8 @@
图片目录地址
git http://39.106.86.127:3000/hb_rd/front_dist.git
branch proj_icampus_dev
powershell 软链形式
```
New-Item -ItemType Junction -Path "src/images" -Target C:\Users\kola\project\school-system\web_dist\images
```

12
dist.ps1

@ -0,0 +1,12 @@
# Change to the src/images directory, or exit if the directory doesn't exist
Set-Location -Path C:\Users\kola\project\school-system\web_dist\images -ErrorAction Stop
# 1. 添加当前目录所有文件(包含.gitignore忽略的文件也一并加入,等价svn --no-ignore --force)
git add --force .
# 2. 提交,备注update
git commit -m "update"
git push origin proj_icampus_dev
ssh hb127 "cd /data1/wwwroot/default/highedu/web_dist_server/dist && git pull && exit"

74
project.private.config.json

@ -4,13 +4,83 @@
"miniprogram": { "miniprogram": {
"list": [ "list": [
{ {
"name": "活动报名结果页", "name": "登录",
"pathName": "pages/actResult/index", "pathName": "pages/login/index",
"query": "", "query": "",
"scene": null, "scene": null,
"launchMode": "default" "launchMode": "default"
}, },
{ {
"name": "我的评论",
"pathName": "pages/myComment/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "我的智能体",
"pathName": "pages/myAgent/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "我的活动",
"pathName": "pages/myAct/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "我的",
"pathName": "pages/my/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "智能体-评价",
"pathName": "pages/agentEva/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "智能体",
"pathName": "pages/agent/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "校园巴士",
"pathName": "pages/buses/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "对话历史",
"pathName": "pages/chatHistory/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "chat",
"pathName": "pages/chat/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "活动报名结果页",
"pathName": "pages/actResult/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "创建活动结果页", "name": "创建活动结果页",
"pathName": "pages/actAddResult/index", "pathName": "pages/actAddResult/index",
"query": "", "query": "",

14
src/app.json

@ -3,6 +3,9 @@
"pages/start/index", "pages/start/index",
"pages/index/index", "pages/index/index",
"pages/act/index", "pages/act/index",
"pages/buses/index",
"pages/agent/index",
"pages/my/index",
"pages/notice/index", "pages/notice/index",
"pages/actDetail/index", "pages/actDetail/index",
"pages/actResult/index", "pages/actResult/index",
@ -10,7 +13,12 @@
"pages/actAddResult/index", "pages/actAddResult/index",
"pages/noticeDetail/index", "pages/noticeDetail/index",
"pages/login/index", "pages/login/index",
"pages/my/index" "pages/chat/index",
"pages/chatHistory/index",
"pages/agentEva/index",
"pages/myAct/index",
"pages/myAgent/index",
"pages/myComment/index"
], ],
"window": { "window": {
"backgroundTextStyle": "light", "backgroundTextStyle": "light",
@ -38,6 +46,10 @@
"text": "通知" "text": "通知"
}, },
{ {
"pagePath": "pages/agent/index",
"text": "活动"
},
{
"pagePath": "pages/my/index", "pagePath": "pages/my/index",
"text": "我的" "text": "我的"
} }

6
src/app.ts

@ -1,9 +1,9 @@
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 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')
@ -14,7 +14,7 @@ App<IAppOption>({
globalData: { globalData: {
url: '', url: '',
upFileUrl: '', upFileUrl: '',
imageUrl: '', imageUrl: 'https://app.gohighedu.cn/images',
Timestamp: new Date().getTime(), Timestamp: new Date().getTime(),

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

@ -0,0 +1,164 @@
# SVG Icon 组件
微信小程序 SVG 组件,支持对 SVG 重新着色。
## 组件路径
`/components/svg-icon/index`
## 安装
在页面或全局 `json` 配置中引入组件:
```json
{
"usingComponents": {
"svg-icon": "/components/svg-icon/index"
}
}
```
## 基础用法
```xml
<!-- 直接使用 SVG 文件 -->
<svg-icon src="/images/icon.svg" />
<!-- 指定缩放模式 -->
<svg-icon src="/images/icon.svg" mode="widthFix" />
```
## 单颜色重新着色
传入 `color` 时,会对 SVG 中所有声明 `fill/stroke` 的元素统一着色:
```xml
<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
### Properties
| 参数 | 说明 | 类型 | 默认值 | 必填 |
| --- | --- | --- | --- | --- |
| `src` | SVG 资源地址(支持本地路径、临时路径、网络资源) | `string` | `''` | 是 |
| `color` | SVG 单一颜色(对所有 fill/stroke 元素统一着色) | `string` | `''` | 否 |
| `colors` | SVG 多颜色配置(支持数组或对象) | `array \| object` | `null` | 否 |
| `mode` | SVG 裁剪、缩放模式(与 `image` 标签相同) | `string` | `''` | 否 |
### Events
| 事件名 | 说明 | 回调参数 |
| --- | --- | --- |
| `bind:error` | 当错误发生时触发 | `event.detail = Error \| { errMsg }` |
| `bind:load` | 当图片载入完毕时触发 | `event.detail = { height, width }` |
### External Classes
| 类名 | 说明 |
| --- | --- |
| `image-class` | `image` 节点样式类 |
## mode 可选值
与微信小程序 `image` 组件的 `mode` 属性相同:
| 值 | 说明 |
| --- | --- |
| `scaleToFill` | 不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素 |
| `aspectFit` | 保持纵横比缩放图片,使图片的长边能完全显示出来 |
| `aspectFill` | 保持纵横比缩放图片,只保证图片的短边能完全显示出来 |
| `widthFix` | 宽度不变,高度自动变化,保持原图宽高比不变 |
| `heightFix` | 高度不变,宽度自动变化,保持原图宽高比不变 |
| `top` | 不缩放图片,只显示图片的顶部区域 |
| `bottom` | 不缩放图片,只显示图片的底部区域 |
| `center` | 不缩放图片,只显示图片的中间区域 |
| `left` | 不缩放图片,只显示图片的左边区域 |
| `right` | 不缩放图片,只显示图片的右边区域 |
| `top left` | 不缩放图片,只显示图片的左上边区域 |
| `top right` | 不缩放图片,只显示图片的右上边区域 |
| `bottom left` | 不缩放图片,只显示图片的左下边区域 |
| `bottom right` | 不缩放图片,只显示图片的右下边区域 |
## 实现原理
1. 读取 SVG 文件内容(本地文件或网络文件)
2. 通过正则替换 SVG 中的 `fill/stroke` 属性值来实现改色
3. 将修改后的 SVG 内容转为 base64 格式,作为 `image``src`
## 注意事项
- SVG 文件必须包含 `fill``stroke` 属性才能被改色
- 网络资源需要配置 `downloadFile` 合法域名
- 组件会缓存下载的网络资源,避免重复下载

34
src/components/svg-icon/base64.ts

@ -0,0 +1,34 @@
/**
* Base64
* SVG base64
*/
// Base64 字符映射表
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
/**
* Base64
* @param str
* @returns Base64
*/
export function encode(str: string): string {
let result = ''
const len = str.length
const remainder = len % 3
for (let i = 0; i < len - remainder; i += 3) {
const n = (str.charCodeAt(i) << 16) | (str.charCodeAt(i + 1) << 8) | str.charCodeAt(i + 2)
result += CHARS[(n >> 18) & 63] + CHARS[(n >> 12) & 63] + CHARS[(n >> 6) & 63] + CHARS[n & 63]
}
// 处理剩余字符
if (remainder === 1) {
const n = str.charCodeAt(len - 1)
result += `${CHARS[(n >> 2) & 63] + CHARS[(n << 4) & 63] }==`
} else if (remainder === 2) {
const n = (str.charCodeAt(len - 2) << 8) | str.charCodeAt(len - 1)
result += `${CHARS[(n >> 10) & 63] + CHARS[(n >> 4) & 63] + CHARS[(n << 2) & 63] }=`
}
return result
}

4
src/components/svg-icon/index.json

@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

7
src/components/svg-icon/index.scss

@ -0,0 +1,7 @@
/**
* SVG Icon 组件样式
*/
.svg-icon {
display: block;
}

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

@ -0,0 +1,156 @@
/**
* SVG Icon
* SVG
*/
import { encode } from './base64'
const fs = wx.getFileSystemManager()
// 临时文件缓存(网络资源下载后缓存)
const tempFileMap = new Map<string, string>()
/**
*
* @param url
* @returns
*/
function downloadFileSync(url: string): Promise<WechatMiniprogram.DownloadFileSuccessCallbackResult> {
return new Promise((resolve, reject) => {
wx.downloadFile({
url,
success: resolve,
fail: reject,
})
})
}
Component({
options: {
styleIsolation: 'shared',
},
externalClasses: ['image-class'],
properties: {
/** SVG 资源地址(支持本地路径、临时路径、网络资源) */
src: {
type: String,
value: '',
},
/** SVG 单一颜色(对所有 fill/stroke 元素统一着色) */
color: {
type: String,
value: '',
},
/** SVG 多颜色配置(支持数组或对象) */
colors: {
type: null,
value: null,
},
/** SVG 裁剪、缩放模式(与 image 标签相同) */
mode: {
type: String,
value: '',
},
},
observers: {
'src, color, colors': async function (src: string, color: string, colors: unknown) {
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
}
// 处理颜色配置
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}"`
})
}
// 设置默认底色(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(/<svg /, `<svg fill="${defaultColor}" `)
}
// 转为 base64 格式
this.setData({
base64: `data:image/svg+xml;base64,${encode(svgData)}`,
})
} else {
// 不需要改色,直接使用原路径
this.setData({ base64: src })
}
} catch (err) {
console.error('SVG 加载失败:', err)
this.triggerEvent('error', err)
}
},
},
data: {
base64: '',
},
methods: {
/**
*
*/
onImageError(e: WechatMiniprogram.ImageError) {
this.triggerEvent('error', e.detail)
},
/**
*
*/
onImageLoad(e: WechatMiniprogram.ImageLoad) {
this.triggerEvent('load', e.detail)
},
},
})
export {}

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

@ -0,0 +1,11 @@
<!--
SVG Icon 组件
支持对 SVG 重新着色
-->
<image
class="svg-icon image-class"
src="{{base64}}"
mode="{{mode}}"
binderror="onImageError"
bindload="onImageLoad"
/>

158
src/components/uploadFile/README.md

@ -0,0 +1,158 @@
# Upload 上传组件 API 文档
## 组件路径
`@/components/uploadFile/index`
## 说明
上传接口地址固定为 `app.globalData.upFileUrl`,组件内部自动附加 `loginState`,无需外部传入。
## Properties 入参
| 参数 | 说明 | 类型 | 默认值 | 必填 |
| --- | --- | --- | --- | --- |
| `maxCount` | 最大上传文件数 | `Number` | `9` | 否 |
| `maxSize` | 单文件最大限制(byte) | `Number` | `10485760`(10MB) | 否 |
| `accept` | 允许的文件类型数组,可选值:`'image'` \| `'video'` \| `'file'` | `Array` | `['image']` | 否 |
| `extensions` | 自定义文件后缀(仅 accept 含 `'file'` 时生效),如 `['.pdf', '.doc']` | `Array` | `[]` | 否 |
| `readonly` | 只读模式(不显示上传和删除按钮) | `Boolean` | `false` | 否 |
| `useSlot` | 是否使用自定义上传区域插槽 | `Boolean` | `false` | 否 |
| `fileList` | 已有文件列表(用于回显) | `Array` | `[]` | 否 |
### fileList 数据结构
```ts
{
uid: string // 文件唯一标识
url: string // 文件路径
type: string // 文件类型: 'image' | 'video' | 'file'
name: string // 文件名
size: number // 文件大小(byte)
}
```
## Events 事件
| 事件名 | 说明 | 回调参数 |
| --- | --- | --- |
| `bind:select` | 选中本地文件后触发 | `{ files: UploadFile[] }` |
| `bind:success` | 单文件上传完成 | `{ file: UploadFile, response: any }` |
| `bind:error` | 上传失败 | `{ file: UploadFile, error: Error }` |
| `bind:remove` | 删除文件 | `{ file: UploadFile, fileList: UploadFile[] }` |
### 事件返回的 UploadFile 结构
```ts
{
uid: string // 文件唯一标识
url: string // 文件路径(上传成功后为远程 url)
type: 'image' | 'video' | 'file' // 文件类型
name: string // 文件名
size: number // 文件大小(byte)
status: 'pending' | 'uploading' | 'success' | 'error' // 上传状态
progress: number // 上传进度 0-100
}
```
## Slots 插槽
| 插槽名 | 说明 |
| --- | --- |
| `upload-area` | 自定义上传区域布局,需同时设置 `useSlot="{{true}}"`。使用时上传触发器尺寸自适应 slot 内容 |
## CSS 变量
| 变量名 | 说明 | 默认值 |
| --- | --- | --- |
| `--upload-bg` | 上传区域背景色 | `#f7f8fa` |
| `--upload-border` | 边框颜色 | `#e5e7eb` |
| `--upload-text` | 主文字颜色 | `#1f2937` |
| `--upload-text-secondary` | 次要文字颜色 | `#9ca3af` |
| `--upload-primary` | 主题色 | `#3b82f6` |
| `--upload-error` | 错误色 | `#ef4444` |
| `--upload-mask` | 遮罩颜色 | `rgba(0, 0, 0, 0.5)` |
| `--upload-radius` | 圆角大小 | `16rpx` |
| `--upload-size` | 文件项尺寸(仅默认上传框生效,slot 模式自适应) | `160rpx` |
## 使用案例
### 案例 1:使用默认上传框
```json
// page.json
{
"usingComponents": {
"upload-file": "@/components/uploadFile/index"
}
}
```
```xml
<!-- page.wxml -->
<upload-file
maxCount="{{3}}"
maxSize="{{5242880}}"
accept="{{['image']}}"
bind:select="onUploadSelect"
bind:success="onUploadSuccess"
bind:error="onUploadError"
bind:remove="onUploadRemove"
/>
```
```ts
// page.ts
Page({
onUploadSelect(e: WechatMiniprogram.CustomEvent) {
console.log('选中文件', e.detail.files)
},
onUploadSuccess(e: WechatMiniprogram.CustomEvent) {
console.log('上传成功', e.detail.file, e.detail.response)
},
onUploadError(e: WechatMiniprogram.CustomEvent) {
console.log('上传失败', e.detail.file, e.detail.error)
},
onUploadRemove(e: WechatMiniprogram.CustomEvent) {
console.log('删除文件', e.detail.file, e.detail.fileList)
},
})
```
### 案例 2:使用 slot 自定义上传区域
```xml
<!-- page.wxml -->
<upload-file
useSlot="{{true}}"
maxCount="{{5}}"
accept="{{['image', 'video']}}"
bind:success="onUploadSuccess"
>
<view slot="upload-area" class="custom-upload-area">
<image src="/images/upload-icon.png" mode="aspectFit" />
<text>点击上传</text>
</view>
</upload-file>
```
```scss
// page.scss
.custom-upload-area {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12rpx;
image {
width: 64rpx;
height: 64rpx;
}
text {
font-size: 24rpx;
color: #9ca3af;
}
}
```

4
src/components/uploadFile/index.json

@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

272
src/components/uploadFile/index.scss

@ -0,0 +1,272 @@
/**
* Upload 上传组件样式
* 简约通用颜色通过 CSS 变量定义方便外部覆盖
*/
/* CSS 变量定义,外部可通过 page 或父级覆盖 */
page {
--upload-bg: #f7f8fa;
--upload-border: #e5e7eb;
--upload-text: #1f2937;
--upload-text-secondary: #9ca3af;
--upload-primary: #3b82f6;
--upload-error: #ef4444;
--upload-mask: rgba(0, 0, 0, 0.5);
--upload-radius: 16rpx;
--upload-size: 160rpx;
--upload-preview-height: 160rpx; // 预览项高度独立控制
}
.upload {
width: 100%;
.upload-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.upload-item {
position: relative;
width: 100%;
height: var(--upload-preview-height);
border-radius: var(--upload-radius);
overflow: hidden;
&--file {
width: 100%;
height: auto;
min-height: var(--upload-preview-height);
}
}
.upload-preview {
width: 100%;
height: 100%;
position: relative;
&--file {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx;
background: var(--upload-bg);
border-radius: var(--upload-radius);
}
.upload-preview-media {
width: 100%;
height: 100%;
display: block;
}
.upload-preview-play {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--upload-mask);
.upload-preview-play-icon {
width: 0;
height: 0;
border-style: solid;
border-width: 16rpx 0 16rpx 28rpx;
border-color: transparent transparent transparent #fff;
margin-left: 8rpx;
}
}
}
/* 文件类型预览 */
.upload-file-icon {
width: 80rpx;
height: 96rpx;
background: #fff;
border: 2rpx solid var(--upload-border);
border-radius: 8rpx;
position: relative;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 12rpx;
flex-shrink: 0;
.upload-file-icon-corner {
position: absolute;
top: 0;
right: 0;
width: 24rpx;
height: 24rpx;
background: var(--upload-border);
border-radius: 0 6rpx 0 8rpx;
}
.upload-file-icon-text {
font-size: 16rpx;
color: var(--upload-text-secondary);
max-width: 60rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.upload-file-name {
flex: 1;
font-size: 26rpx;
color: var(--upload-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 上传中遮罩 */
.upload-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--upload-mask);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12rpx;
.upload-progress {
width: 70%;
height: 6rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 3rpx;
overflow: hidden;
.upload-progress-bar {
height: 100%;
background: #fff;
border-radius: 3rpx;
transition: width 0.2s ease;
}
}
.upload-progress-text {
font-size: 24rpx;
color: #fff;
}
&--error {
.upload-error-text {
font-size: 24rpx;
color: #fff;
}
.upload-retry {
padding: 8rpx 24rpx;
border: 2rpx solid #fff;
border-radius: 24rpx;
.upload-retry-text {
font-size: 24rpx;
color: #fff;
}
}
}
}
/* 删除按钮 */
.upload-remove {
position: absolute;
top: -8rpx;
right: -8rpx;
width: 36rpx;
height: 36rpx;
background: var(--upload-mask);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
.upload-remove-icon {
position: relative;
width: 18rpx;
height: 18rpx;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 2rpx;
background: #fff;
transform-origin: center;
}
&::before {
transform: translateY(-50%) rotate(45deg);
}
&::after {
transform: translateY(-50%) rotate(-45deg);
}
}
}
/* 上传触发器 */
.upload-trigger {
width: var(--upload-size);
height: var(--upload-size);
&--slot {
width: 100%; // slot 模式下宽度撑满父容器
height: auto; // 高度自适应 slot 内容
}
.upload-trigger-default {
width: 100%;
height: 100%;
background: var(--upload-bg);
border-radius: var(--upload-radius);
display: flex;
align-items: center;
justify-content: center;
border: 2rpx dashed var(--upload-border);
box-sizing: border-box;
.upload-trigger-icon {
position: relative;
width: 48rpx;
height: 48rpx;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
background: var(--upload-text-secondary);
border-radius: 2rpx;
}
&::before {
width: 48rpx;
height: 4rpx;
transform: translate(-50%, -50%);
}
&::after {
width: 4rpx;
height: 48rpx;
transform: translate(-50%, -50%);
}
}
}
}
}

363
src/components/uploadFile/index.ts

@ -0,0 +1,363 @@
/**
* Upload
* //
*/
/** 单个文件对象 */
interface UploadFile {
/** 文件唯一标识 */
uid: string
/** 本地临时路径或上传后的远程 url */
url: string
/** 文件类型: image | video | file */
type: string
/** 文件名 */
name: string
/** 文件大小(byte) */
size: number
/** 上传状态: pending | uploading | success | error */
status: 'pending' | 'uploading' | 'success' | 'error'
/** 上传进度 0-100 */
progress: number
/** 上传任务对象 */
_task?: WechatMiniprogram.UploadTask
}
/** 文件大小格式化 */
function formatSize(size: number): string {
if (size < 1024) return `${size}B`
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)}KB`
return `${(size / (1024 * 1024)).toFixed(1)}MB`
}
Component({
options: {
multipleSlots: true,
styleIsolation: 'shared', // 允许外部样式影响组件内部
},
properties: {
/** 最大上传文件数,默认 9 */
maxCount: {
type: Number,
value: 9,
},
/** 单文件最大限制(byte),默认 10MB */
maxSize: {
type: Number,
value: 10 * 1024 * 1024,
},
/** 允许的文件类型数组: 'image' | 'video' | 'file' */
accept: {
type: Array,
value: ['image'],
},
/** 自定义文件后缀(仅 accept 含 file 时生效),如 ['.pdf', '.doc', '.docx'] */
extensions: {
type: Array,
value: [],
},
/** 只读模式(不显示上传和删除按钮) */
readonly: {
type: Boolean,
value: false,
},
/** 是否使用自定义上传区域插槽 */
useSlot: {
type: Boolean,
value: false,
},
/** 已有文件列表(用于回显) */
fileList: {
type: Array,
value: [],
},
},
data: {
/** 内部文件列表 */
_fileList: [] as UploadFile[],
},
lifetimes: {
attached() {
// 初始化已有文件列表
if (this.properties.fileList.length > 0) {
this.setData({
_fileList: this.properties.fileList.map((item: any) => ({
uid: item.uid || `init_${Date.now()}_${Math.random()}`,
url: item.url,
type: item.type || 'image',
name: item.name || '',
size: item.size || 0,
status: 'success',
progress: 100,
})),
})
}
},
},
methods: {
/**
*
*/
onChooseFile() {
if (this.data.readonly) return
const { _fileList, maxCount } = this.data
const remaining = maxCount - _fileList.length
if (remaining <= 0) {
wx.showToast({ title: `最多上传${maxCount}个文件`, icon: 'none' })
return
}
const accept = this.properties.accept as string[]
// 如果包含 image 或 video,使用 wx.chooseMedia
const hasImage = accept.includes('image')
const hasVideo = accept.includes('video')
const hasFile = accept.includes('file')
if (hasFile && !hasImage && !hasVideo) {
// 纯文件上传
this.chooseFileOnly(remaining)
} else if (hasImage || hasVideo) {
// 图片/视频
this.chooseMedia(remaining, hasImage, hasVideo, hasFile)
}
},
/**
* /
*/
chooseMedia(remaining: number, hasImage: boolean, hasVideo: boolean, hasFile: boolean) {
const mediaType: string[] = []
if (hasImage) mediaType.push('image')
if (hasVideo) mediaType.push('video')
wx.chooseMedia({
count: remaining,
mediaType: mediaType as ('image' | 'video')[],
sourceType: ['album', 'camera'],
camera: 'back',
success: (res) => {
const files = res.tempFiles.map((f) => {
const isVideo = f.fileType === 'video'
return {
uid: `file_${Date.now()}_${Math.random()}`,
url: f.tempFilePath,
type: isVideo ? 'video' : 'image',
name: f.tempFilePath.split('/').pop() || '',
size: f.size,
status: 'pending' as const,
progress: 0,
}
})
this.afterChoose(files)
},
fail: (err) => {
if (!err.errMsg.includes('cancel')) {
wx.showToast({ title: '选择文件失败', icon: 'none' })
}
},
})
// 如果同时支持文件,提示用户
if (hasFile) {
// 在 chooseMedia 之后无法同时选择文件,这里不做额外处理
// 如果需要同时支持,用户可分多次选择
}
},
/**
*
*/
chooseFileOnly(remaining: number) {
const extensions = this.properties.extensions as string[]
wx.chooseMessageFile({
count: remaining,
type: extensions.length > 0 ? 'file' : 'file',
extension: extensions.length > 0 ? extensions : undefined,
success: (res) => {
const files: UploadFile[] = res.tempFiles.map((f) => ({
uid: `file_${Date.now()}_${Math.random()}`,
url: f.path,
type: 'file',
name: f.name,
size: f.size,
status: 'pending',
progress: 0,
}))
this.afterChoose(files)
},
fail: (err) => {
if (!err.errMsg.includes('cancel')) {
wx.showToast({ title: '选择文件失败', icon: 'none' })
}
},
})
},
/**
*
*/
afterChoose(files: UploadFile[]) {
// 校验文件大小
const validFiles: UploadFile[] = []
const maxSize = this.properties.maxSize
for (const file of files) {
if (file.size > maxSize) {
wx.showToast({
title: `${file.name || '文件'}超过${formatSize(maxSize)}`,
icon: 'none',
})
continue
}
validFiles.push(file)
}
if (validFiles.length === 0) return
// 触发 onSelect 事件
this.triggerEvent('select', { files: validFiles })
// 更新文件列表
const newList = [...this.data._fileList, ...validFiles]
this.setData({ _fileList: newList })
// 逐个上传
validFiles.forEach((file) => {
this.uploadFile(file)
})
},
/**
*
*/
uploadFile(file: UploadFile) {
const app = getApp<IAppOption>()
const action = app.globalData.upFileUrl
const loginState = app.globalData.loginState
this.updateFile(file.uid, { status: 'uploading', progress: 0 })
const task = wx.uploadFile({
url: action,
filePath: file.url,
name: 'file',
header: { loginState },
formData: { loginState },
success: (res) => {
// 尝试解析返回数据
let data: any
try {
data = JSON.parse(res.data)
} catch {
data = res.data
}
this.updateFile(file.uid, {
status: 'success',
progress: 100,
url: data.url || data.data?.url || file.url,
})
this.triggerEvent('success', { file: this.getFile(file.uid), response: data })
},
fail: (err) => {
this.updateFile(file.uid, { status: 'error', progress: 0 })
this.triggerEvent('error', { file: this.getFile(file.uid), error: err })
},
})
// 监听上传进度
task.onProgressUpdate((res) => {
this.updateFile(file.uid, { progress: res.progress })
})
// 保存 task 引用
this.updateFile(file.uid, { _task: task })
},
/**
*
*/
onRetry(e: WechatMiniprogram.BaseEvent) {
const { uid } = e.currentTarget.dataset
const file = this.getFile(uid)
if (file && file.status === 'error') {
this.uploadFile(file)
}
},
/**
*
*/
onRemove(e: WechatMiniprogram.BaseEvent) {
const { uid } = e.currentTarget.dataset
const file = this.getFile(uid)
if (!file) return
// 取消上传任务
if (file._task) {
file._task.abort()
}
const newList = this.data._fileList.filter((f) => f.uid !== uid)
this.setData({ _fileList: newList })
this.triggerEvent('remove', { file, fileList: newList })
},
/**
*
*/
onPreviewImage(e: WechatMiniprogram.BaseEvent) {
const { uid } = e.currentTarget.dataset
const file = this.getFile(uid)
if (!file) return
const imageList = this.data._fileList.filter((f) => f.type === 'image').map((f) => f.url)
wx.previewImage({
current: file.url,
urls: imageList,
})
},
/**
*
*/
onPreviewVideo(e: WechatMiniprogram.BaseEvent) {
const { uid } = e.currentTarget.dataset
const file = this.getFile(uid)
if (!file) return
wx.previewMedia({
sources: [{ url: file.url, type: 'video' }],
current: 0,
})
},
/**
*
*/
updateFile(uid: string, patch: Partial<UploadFile>) {
const list = this.data._fileList.map((f) => {
if (f.uid === uid) {
return { ...f, ...patch }
}
return f
})
this.setData({ _fileList: list })
},
/**
*
*/
getFile(uid: string): UploadFile | undefined {
return this.data._fileList.find((f) => f.uid === uid)
},
/**
*
*/
getFileList(): UploadFile[] {
return this.data._fileList
},
},
})

92
src/components/uploadFile/index.wxml

@ -0,0 +1,92 @@
<!--
Upload 上传组件
支持图片/视频/文件上传,进度条,预览,删除,重试
-->
<view class="upload">
<!-- 文件列表 -->
<view class="upload-list">
<!-- 单个文件项 -->
<view
wx:for="{{_fileList}}"
wx:key="uid"
class="upload-item upload-item--{{item.type}}"
>
<!-- 图片预览 -->
<view
wx:if="{{item.type === 'image'}}"
class="upload-preview"
data-uid="{{item.uid}}"
bindtap="onPreviewImage"
>
<image class="upload-preview-media" src="{{item.url}}" mode="aspectFill" />
</view>
<!-- 视频预览 -->
<view
wx:elif="{{item.type === 'video'}}"
class="upload-preview"
data-uid="{{item.uid}}"
bindtap="onPreviewVideo"
>
<video
class="upload-preview-media"
src="{{item.url}}"
controls="{{false}}"
object-fit="cover"
/>
<view class="upload-preview-play">
<view class="upload-preview-play-icon"></view>
</view>
</view>
<!-- 文件预览 -->
<view wx:elif="{{item.type === 'file'}}" class="upload-preview upload-preview--file">
<view class="upload-file-icon">
<view class="upload-file-icon-corner"></view>
<text class="upload-file-icon-text">{{item.name}}</text>
</view>
<text class="upload-file-name">{{item.name}}</text>
</view>
<!-- 上传中遮罩 + 进度条 -->
<view wx:if="{{item.status === 'uploading'}}" class="upload-mask">
<view class="upload-progress">
<view class="upload-progress-bar" style="width: {{item.progress}}%"></view>
</view>
<text class="upload-progress-text">{{item.progress}}%</text>
</view>
<!-- 上传失败遮罩 + 重试 -->
<view wx:if="{{item.status === 'error'}}" class="upload-mask upload-mask--error">
<text class="upload-error-text">上传失败</text>
<view class="upload-retry" data-uid="{{item.uid}}" catchtap="onRetry">
<text class="upload-retry-text">重试</text>
</view>
</view>
<!-- 删除按钮 -->
<view
wx:if="{{!readonly}}"
class="upload-remove"
data-uid="{{item.uid}}"
catchtap="onRemove"
>
<view class="upload-remove-icon"></view>
</view>
</view>
<!-- 默认上传按钮(未达到最大数量且非只读时显示) -->
<view
wx:if="{{!readonly && _fileList.length < maxCount}}"
class="upload-trigger {{useSlot ? 'upload-trigger--slot' : ''}}"
bindtap="onChooseFile"
>
<!-- 具名插槽:自定义上传区域 -->
<slot name="upload-area"></slot>
<!-- 默认上传占位 -->
<view wx:if="{{!useSlot}}" class="upload-trigger-default">
<view class="upload-trigger-icon"></view>
</view>
</view>
</view>
</view>

2
src/custom-tab-bar/index.ts

@ -29,7 +29,7 @@ Component({
iconActive: 'tabbar13', iconActive: 'tabbar13',
}, },
{ {
pagePath: '/pages/knowledge/index', pagePath: '/pages/agent/index',
text: '智能体', text: '智能体',
icon: 'tabbar4', icon: 'tabbar4',
iconActive: 'tabbar14', iconActive: 'tabbar14',

BIN
src/images/bg3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

BIN
src/images/bg4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
src/images/bg5.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
src/images/bg6.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
src/images/gif1.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
src/images/icon43.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
src/images/icon44.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
src/images/icon45.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
src/images/icon46.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
src/images/icon47.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
src/images/icon48.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/images/icon49.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/images/icon50.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/images/icon51.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/images/icon52.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
src/images/icon53.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
src/images/icon54.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

BIN
src/images/icon55.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/images/icon56.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
src/images/icon57.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src/images/icon58.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
src/images/icon59.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
src/images/icon60.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
src/images/icon61.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
src/images/icon62.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/images/icon63.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/images/icon64.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
src/images/icon65.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
src/images/icon66.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
src/images/icon67.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

BIN
src/images/icon68.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

12
src/images/svg1.svg

@ -0,0 +1,12 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Mask group">
<mask id="mask0_331_3718" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="18" height="18">
<rect id="Rectangle 34626347" width="18" height="18" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_331_3718)">
<g id="Container">
<path id="Icon" d="M8.84168 0C4.97764 0 1.5 2.9195 1.5 7.51342C1.5 10.39 3.77549 13.8247 8.24061 17.7746C8.58408 18.0751 9.09928 18.0751 9.44276 17.7746C13.9079 13.8247 16.1834 10.39 16.1834 7.51342C16.1834 2.9195 12.7057 0 8.84168 0ZM8.84168 9.1449C7.8542 9.1449 6.99553 8.32916 6.99553 7.29875C6.99553 6.31127 7.8542 5.49553 8.84168 5.49553C9.82916 5.49553 10.6878 6.31127 10.6878 7.29875C10.6878 8.32916 9.82916 9.1449 8.84168 9.1449Z" fill="#671FAB"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 836 B

4
src/pages/act/index.scss

@ -186,8 +186,10 @@ page {
} }
} }
.wrap { .wrap {
display: flex;
flex-direction: column;
justify-content: space-between;
.title { .title {
margin-bottom: 28rpx;
font-size: 32rpx; font-size: 32rpx;
color: rgba(17, 24, 39, 1); color: rgba(17, 24, 39, 1);
line-height: 48rpx; line-height: 48rpx;

4
src/pages/actAdd/index.json

@ -1,5 +1,7 @@
{ {
"navigationBarTitleText": "创建活动", "navigationBarTitleText": "创建活动",
"navigationStyle": "default", "navigationStyle": "default",
"usingComponents": {} "usingComponents": {
"upload-file": "/components/uploadFile/index"
}
} }

16
src/pages/actAdd/index.scss

@ -1,5 +1,11 @@
page { page {
background-color: #f5f7fa; background-color: #f5f7fa;
// uploadFile 组件设置 CSS 变量
--upload-size: 100%;
--upload-preview-height: 350rpx; // 预览项高度
--upload-radius: 16rpx;
--upload-bg: rgba(247, 249, 251, 1);
--upload-border: #d1d5db;
} }
.page { .page {
@ -244,14 +250,15 @@ page {
} }
/* ========== 上传 ========== */ /* ========== 上传 ========== */
.upload-box { .upload-inner {
height: 220rpx; width: 100%;
height: 350rpx; height: 350rpx;
border: 2rpx dashed #d1d5db; border: 2rpx dashed #d1d5db;
border-radius: 16rpx; border-radius: 16rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-direction: column;
overflow: hidden; overflow: hidden;
background-color: rgba(247, 249, 251, 1); background-color: rgba(247, 249, 251, 1);
@ -260,10 +267,6 @@ page {
border-color: transparent; border-color: transparent;
} }
.upload-inner {
display: flex;
flex-direction: column;
align-items: center;
.upload-camera { .upload-camera {
width: 45rpx; width: 45rpx;
height: 45rpx; height: 45rpx;
@ -273,7 +276,6 @@ page {
font-size: 24rpx; font-size: 24rpx;
color: rgba(148, 163, 184, 0.5); color: rgba(148, 163, 184, 0.5);
} }
}
.upload-img { .upload-img {
width: 100%; width: 100%;

17
src/pages/actAdd/index.ts

@ -17,7 +17,7 @@ Page({
], ],
// 步骤1 基本信息 // 步骤1 基本信息
coverImage: '', coverImageList: [] as Array<{ uid: string, url: string, type: string, name: string, size: number }>,
title: '', title: '',
startTime: '', startTime: '',
endTime: '', endTime: '',
@ -69,16 +69,13 @@ Page({
}, },
// ========== 图片上传 ========== // ========== 图片上传 ==========
onChooseImage() { onCoverUploadSuccess(e: WechatMiniprogram.CustomEvent) {
wx.chooseMedia({ const { file } = e.detail
count: 1, this.setData({ coverImageList: [file] })
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFile = res.tempFiles[0]?.tempFilePath
if (tempFile) this.setData({ coverImage: tempFile })
}, },
})
onCoverRemove(_e: WechatMiniprogram.CustomEvent) {
this.setData({ coverImageList: [] })
}, },
// ========== 输入绑定 ========== // ========== 输入绑定 ==========

16
src/pages/actAdd/index.wxml

@ -26,15 +26,19 @@
<text>活动头图</text> <text>活动头图</text>
<text class="required">*</text> <text class="required">*</text>
</view> </view>
<view class="upload-box {{coverImage ? 'has-image' : ''}}" bindtap="onChooseImage"> <upload-file
<image wx:if="{{coverImage}}" class="upload-img" src="{{coverImage}}" mode="aspectFill" /> maxCount="{{1}}"
<block wx:else> accept="{{['image']}}"
<view class="upload-inner"> useSlot="{{true}}"
fileList="{{coverImageList}}"
bind:success="onCoverUploadSuccess"
bind:remove="onCoverRemove"
>
<view slot="upload-area" class="upload-inner">
<image class="upload-camera" src="/images/icon35.png"></image> <image class="upload-camera" src="/images/icon35.png"></image>
<text class="upload-text">点击上传</text> <text class="upload-text">点击上传</text>
</view> </view>
</block> </upload-file>
</view>
</view> </view>
<view class="form-field"> <view class="form-field">

4
src/pages/actDetail/index.json

@ -4,6 +4,8 @@
"van-count-down": "@vant/weapp/count-down/index", "van-count-down": "@vant/weapp/count-down/index",
"mp-html": "mp-html", "mp-html": "mp-html",
"van-rate": "@vant/weapp/rate/index", "van-rate": "@vant/weapp/rate/index",
"popup": "/components/popup/index" "popup": "/components/popup/index",
"van-popup": "@vant/weapp/popup/index",
"uploadFile": "/components/uploadFile/index"
} }
} }

122
src/pages/actDetail/index.scss

@ -79,7 +79,7 @@ page {
} }
.row-wrap { .row-wrap {
margin-top: 24rpx; margin-top: 24rpx;
border-top: 1px solid rgba(136, 142, 152, 0.19); border-top: 1rpx solid rgba(136, 142, 152, 0.19);
.row { .row {
margin-top: 34rpx; margin-top: 34rpx;
display: flex; display: flex;
@ -155,7 +155,7 @@ page {
gap: 2rpx; gap: 2rpx;
.line-top, .line-top,
.line-bottom { .line-bottom {
border-right: 1px dashed rgba(74, 184, 253, 1); border-right: 1rpx dashed rgba(74, 184, 253, 1);
} }
.line-top { .line-top {
height: 10rpx; height: 10rpx;
@ -367,7 +367,7 @@ page {
} }
.com { .com {
color: rgba(74, 184, 253, 1); color: rgba(74, 184, 253, 1);
border: 1px solid #4ab8fd; border: 1rpx solid #4ab8fd;
} }
.btn { .btn {
color: #fff; color: #fff;
@ -377,12 +377,122 @@ page {
} }
} }
.comment-popup {
padding: 32rpx;
.title {
font-size: 36rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.rate-wrap {
margin-top: 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
.rate {
display: flex;
align-items: center;
gap: 32rpx;
.num {
font-size: 32rpx;
color: rgba(254, 181, 74, 1);
}
}
.status {
font-size: 28rpx;
color: rgba(254, 181, 74, 1);
}
}
.area-wrap {
padding: 30rpx;
margin-top: 28rpx;
background-color: rgba(247, 248, 250, 1);
border-radius: 24rpx;
.txa {
height: 130rpx;
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
}
.txa-place {
color: rgba(148, 163, 184, 1);
}
.upload-list {
margin-top: 20rpx;
.upload {
width: 108rpx;
height: 108rpx;
background-color: #fff;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
.icon {
width: 52rpx;
height: 45rpx;
}
}
}
}
.anonymous {
margin-top: 44rpx;
font-size: 28rpx;
color: #111827;
display: flex;
align-items: center;
.content {
color: #94a3b8;
}
.wx-checkbox-input {
width: 30rpx;
height: 30rpx;
border-radius: 8rpx;
&.wx-checkbox-input-checked {
background: #4ab8fd;
border-color: #4ab8fd;
&::before {
color: #fff;
}
}
}
}
.c-footer {
margin-top: 44rpx;
display: flex;
align-items: center;
gap: 30rpx;
.cancel {
width: 329rpx;
height: 96rpx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 16rpx;
border: 1px solid #4ab8fd;
font-size: 30rpx;
color: #4ab8fd;
box-sizing: border-box;
}
.submit {
width: 329rpx;
height: 96rpx;
display: flex;
justify-content: center;
align-items: center;
border-radius: 16rpx;
font-size: 30rpx;
color: #fff;
box-shadow: 0px 15px 30px -6px rgba(74, 172, 219, 0.4);
background: linear-gradient(90deg, #9ddffd 0%, #4ab8fd 100%);
}
}
}
.slidebar-share { .slidebar-share {
position: fixed; position: fixed;
right: 16rpx; right: 16rpx;
bottom: 200rpx; bottom: 320rpx;
width: 180rpx; width: 160rpx;
height: 180rpx; height: 160rpx;
.icon { .icon {
width: 100%; width: 100%;
height: 100%; height: 100%;

7
src/pages/actDetail/index.ts

@ -5,6 +5,8 @@ Page({
popupShow: false, popupShow: false,
popupType: 'popup1', // 签到成功弹窗 popupType: 'popup1', // 签到成功弹窗
popupParams: {} as any, popupParams: {} as any,
commentShow: false,
}, },
onLoad() {}, onLoad() {},
handlePopupOk() { handlePopupOk() {
@ -21,6 +23,11 @@ Page({
popupType: 'i', popupType: 'i',
}) })
}, },
onCommentClose() {
this.setData({
commentShow: false,
})
},
}) })
export {} export {}

32
src/pages/actDetail/index.wxml

@ -151,6 +151,38 @@
<image class="icon" src="/images/icon42.png"></image> <image class="icon" src="/images/icon42.png"></image>
</view> </view>
<!-- 评论控件 -->
<van-popup show="{{ commentShow }}" position="bottom" round bind:close="onCommentClose">
<view class="comment-popup">
<view class="title">评分</view>
<view class="rate-wrap">
<view class="rate">
<van-rate value="{{4.5}}" color="#F7B550" allow-half size="46rpx" void-color="#F7F8FA" />
<view class="num">4.5</view>
</view>
<view class="status">非常满意</view>
</view>
<view class="area-wrap">
<textarea class="txa" placeholder-class="txa-place" placeholder="请聊聊本次活动的感受"></textarea>
<view class="upload-list">
<uploadFile maxNum="{{3}}" useSlot>
<view class="upload" slot="upload-area">
<image class="icon" src="/images/icon43.png"></image>
</view>
</uploadFile>
</view>
</view>
<checkbox class="anonymous" color="#4AB8FD">
匿名评价
<text class="content">你的头像、昵称将隐藏</text>
</checkbox>
<view class="c-footer">
<view class="cancel">取消</view>
<view class="submit">发布</view>
</view>
</view>
</van-popup>
<popup <popup
show="{{popupShow}}" show="{{popupShow}}"
type="{{popupType}}" type="{{popupType}}"

9
src/pages/agent/index.json

@ -0,0 +1,9 @@
{
"navigationStyle": "default",
"navigationBarTitleText": "智能体",
"usingComponents": {
"van-icon": "@vant/weapp/icon/index",
"van-tab": "@vant/weapp/tab/index",
"van-tabs": "@vant/weapp/tabs/index"
}
}

250
src/pages/agent/index.scss

@ -0,0 +1,250 @@
page {
background-color: rgba(247, 248, 250, 1);
}
.page {
padding-bottom: calc(60px + env(safe-area-inset-bottom));
.page-header {
padding: 20rpx 32rpx;
border-radius: 0 0 32rpx 32rpx;
background-color: #fff;
.search {
padding: 12rpx 24rpx;
display: flex;
align-items: center;
gap: 8rpx;
border-radius: 46rpx;
background-color: rgba(247, 248, 250, 1);
.icon {
width: 40rpx;
height: 40rpx;
}
.input {
flex: 1;
color: rgba(71, 85, 105, 1);
font-size: 28rpx;
}
.input-place {
color: rgba(148, 163, 184, 0.7);
}
}
}
.page-container {
padding: 24rpx 32rpx;
.banner {
.swiper {
width: 100%;
height: 229rpx;
}
.s-img {
display: block;
width: 100%;
height: 100%;
border-radius: 24rpx;
}
}
.kkd {
margin-top: 24rpx;
padding: 32rpx;
background: linear-gradient(180deg, #f2faff 0%, #ffffff 27.29%, #ffffff 100%);
border-radius: 24rpx 24rpx 24rpx 24rpx;
border: 2rpx solid #ffffff;
.k-header {
display: flex;
align-items: center;
justify-content: space-between;
.name {
font-size: 36rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.more {
font-size: 28rpx;
color: rgba(148, 163, 184, 0.5);
}
}
.k-body {
margin-top: 24rpx;
overflow-x: auto;
display: flex;
flex-wrap: nowrap;
gap: 10rpx;
&::-webkit-scrollbar {
display: none;
}
.k-item {
padding: 0 10rpx;
flex-shrink: 0;
width: 170rpx;
text-align: center;
box-sizing: border-box;
.icon {
width: 104rpx;
height: 104rpx;
}
.name {
font-size: 32rpx;
line-height: 42rpx;
color: rgba(71, 85, 105, 1);
text-align: center;
}
}
}
}
.kkd2 {
margin-top: 30rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 22rpx;
.k-row {
padding: 24rpx;
background: linear-gradient(358.94deg, #ffffff 1.04%, #ffffff 59.35%, #f0faff 96.54%);
border-radius: 24rpx;
overflow: hidden;
.kr-header {
font-size: 28rpx;
line-height: 32rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.kr-body {
margin-top: 24rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
.kr-item {
padding: 0 10rpx;
flex-shrink: 0;
text-align: center;
box-sizing: border-box;
.icon {
width: 75rpx;
height: 75rpx;
}
.name {
margin-top: 15rpx;
font-size: 20rpx;
color: rgba(71, 85, 105, 1);
text-align: center;
}
}
}
}
}
.tabs {
.nav-class {
background: rgba(247, 248, 250, 1);
}
.tab-class {
font-size: 32rpx;
}
.tab-active-class {
font-size: 36rpx;
}
.van-tabs__line {
width: 32rpx !important;
height: 6rpx !important;
}
.list {
margin-top: 24rpx;
padding: 22rpx;
background-color: #fff;
border-radius: 24rpx;
.list-card {
display: flex;
align-content: inherit;
padding-bottom: 34rpx;
border-bottom: 1px solid rgba(248, 250, 252, 1);
margin-bottom: 32rpx;
&:last-of-type {
border: none;
margin-bottom: 0;
}
.order {
flex-shrink: 0;
padding-top: 54rpx;
flex-shrink: 0;
min-width: 50rpx;
.icon {
width: 50rpx;
height: 50rpx;
}
.num {
text-align: center;
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
}
}
.photo {
flex-shrink: 0;
width: 158rpx;
height: 158rpx;
display: flex;
align-items: center;
justify-content: center;
.p-img {
width: 136rpx;
height: 136rpx;
}
}
.wrap {
flex: 1;
min-width: 0;
padding-top: 10rpx;
padding-left: 13rpx;
.title {
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.content {
margin-top: 12rpx;
font-size: 28rpx;
color: rgba(148, 163, 184, 1);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.stat {
margin-top: 12rpx;
font-size: 22rpx;
color: rgba(74, 184, 253, 1);
}
.options {
margin-top: 32rpx;
display: flex;
justify-content: flex-end;
gap: 15rpx;
.o-item {
padding: 8rpx 22rpx;
border: 1px solid rgba(74, 184, 253, 1);
border-radius: 40rpx;
display: flex;
align-items: center;
gap: 8rpx;
font-size: 28rpx;
color: rgba(74, 184, 253, 1);
line-height: 32rpx;
.icon {
width: 24rpx;
height: 24rpx;
}
.icon:last-of-type {
display: none;
}
&.active {
background-color: rgba(74, 184, 253, 0.1);
.icon {
display: none;
}
.icon:last-of-type {
display: block;
}
}
}
}
}
}
}
}
}
}

13
src/pages/agent/index.ts

@ -0,0 +1,13 @@
const _app = getApp<IAppOption>()
Page({
data: {},
onLoad() {},
handleEva() {
wx.navigateTo({
url: '/pages/agentEva/index',
})
},
})
export {}

112
src/pages/agent/index.wxml

@ -0,0 +1,112 @@
<view class="page">
<view class="page-header">
<view class="search">
<image class="icon" src="/images/icon47.png"></image>
<input
class="input"
placeholder-class="input-place"
type="text"
placeholder="请搜索你想要的内容"
confirm-type="search"
/>
</view>
</view>
<view class="page-container">
<view class="banner">
<swiper
class="swiper"
indicator-dots
indicator-color="rgba(255, 255, 255, 1)"
indicator-active-color="rgba(74, 184, 253, 1)"
>
<swiper-item class="swiper-item">
<image class="s-img" mode="aspectFill" src="/images/bg1.png"></image>
</swiper-item>
</swiper>
</view>
<view class="kkd">
<view class="k-header">
<view class="name">推荐智能体</view>
<view class="more">
更多
<van-icon name="arrow" />
</view>
</view>
<view class="k-body">
<view class="k-item" wx:for="{{4}}" wx:key="index">
<image class="icon" src="/images/icon2.png"></image>
<view class="name">PPT小助手</view>
</view>
</view>
</view>
<view class="kkd2">
<view class="k-row">
<view class="kr-header">最近使用</view>
<view class="kr-body">
<view class="kr-item" wx:for="{{3}}" wx:key="index">
<image class="icon" src="/images/icon2.png"></image>
<view class="name">PPT小助</view>
</view>
</view>
</view>
<view class="k-row">
<view class="kr-header">我的收藏</view>
<view class="kr-body">
<view class="kr-item" wx:for="{{3}}" wx:key="index">
<image class="icon" src="/images/icon2.png"></image>
<view class="name">文章去</view>
</view>
</view>
</view>
</view>
<van-tabs
class="tabs"
nav-class="nav-class"
tab-class="tab-class"
tab-active-class="tab-active-class"
color="rgba(74, 184, 253, 1)"
title-active-color="rgba(74, 184, 253, 1)"
title-inactive-color="rgba(71, 85, 105, 1)"
active="{{ active }}"
bind:change="onChange"
>
<van-tab title="全部">
<view class="list">
<view class="list-card" wx:for="{{10}}" wx:key="index">
<view class="order">
<image class="icon" wx:if="{{index==0}}" src="/images/icon48.png"></image>
<image class="icon" wx:elif="{{index==1}}" src="/images/icon49.png"></image>
<image class="icon" wx:elif="{{index==2}}" src="/images/icon50.png"></image>
<view class="num" wx:else>{{index+1}}</view>
</view>
<view class="photo">
<image class="p-img" src="/images/icon2.png"></image>
</view>
<view class="wrap">
<view class="title">应用包生成助手</view>
<view class="content">智能规划课表,轻松掌握学习轻松掌握学习</view>
<view class="stat">15.6w人使用</view>
<view class="options">
<view class="o-item active" bind:tap="handleEva">
<image class="icon" src="/images/icon55.png"></image>
<image class="icon" src="/images/icon51.png"></image>
评级
</view>
<view class="o-item">
<image class="icon" src="/images/icon56.png"></image>
<image class="icon" src="/images/icon57.png"></image>
收藏
</view>
<view class="o-item">使用</view>
</view>
</view>
</view>
</view>
</van-tab>
<van-tab title="全部">全部</van-tab>
<van-tab title="全部">全部</van-tab>
<van-tab title="全部">全部</van-tab>
<van-tab title="全部">全部</van-tab>
</van-tabs>
</view>
</view>

7
src/pages/agentEva/index.json

@ -0,0 +1,7 @@
{
"navigationStyle": "default",
"navigationBarTitleText": "智能体",
"usingComponents": {
"van-rate": "@vant/weapp/rate/index"
}
}

219
src/pages/agentEva/index.scss

@ -0,0 +1,219 @@
page {
background-color: rgba(248, 246, 246, 1);
}
.page {
padding-bottom: 420rpx;
.page-header {
padding: 40rpx;
background-color: #fff;
.logo {
display: block;
margin: 0 auto;
width: 160rpx;
height: 160rpx;
box-shadow: 0px 6.7px 10.04px -6.7px rgba(0, 0, 0, 0.1);
box-shadow: 0px 16.74px 25.11px -5.02px rgba(0, 0, 0, 0.1);
border-radius: 24rpx;
}
.title {
margin-top: 26rpx;
text-align: center;
font-size: 40rpx;
color: rgba(17, 24, 39, 1);
}
.content {
margin-top: 6rpx;
font-size: 28rpx;
color: rgba(107, 114, 128, 1);
text-align: center;
}
.rate-wrap {
margin-top: 76rpx;
display: flex;
align-items: center;
justify-content: space-between;
.num {
font-size: 96rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.wrap {
.stat {
margin-top: 5rpx;
font-size: 24rpx;
color: rgba(107, 114, 128, 1);
text-align: right;
}
}
}
}
.list {
margin: 48rpx 30rpx;
.list-title {
font-size: 36rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.list-card {
margin: 30rpx 0 0;
padding: 32rpx;
background-color: rgba(255, 255, 255, 1);
border-radius: 32rpx;
.user {
display: flex;
gap: 20rpx;
.avatar {
flex-shrink: 0;
width: 89rpx;
height: 89rpx;
border-radius: 50%;
}
.wrap {
flex: 1;
padding: 7rpx 20rpx 0;
.w-header {
display: flex;
align-items: center;
gap: 13rpx;
.name {
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.idenity {
padding: 6rpx 11rpx;
font-size: 24rpx;
line-height: 1;
border-radius: 7rpx;
color: rgba(74, 184, 253, 1);
background-color: rgba(74, 184, 253, 0.1);
&.teacher {
background-color: rgba(254, 181, 74, 0.1);
color: rgba(254, 181, 74, 1);
}
}
}
.date {
margin-top: 6rpx;
font-size: 24rpx;
color: rgba(107, 114, 128, 1);
}
}
.rate-num {
align-self: start;
padding: 4rpx 14rpx;
font-size: 24rpx;
font-weight: bold;
color: rgba(74, 184, 253, 1);
background: rgba(74, 184, 253, 0.1);
border-radius: 13rpx;
}
}
.content {
margin-top: 22rpx;
font-size: 28rpx;
color: rgba(31, 41, 55, 1);
line-height: 42rpx;
}
.l-footer {
margin-top: 24rpx;
display: flex;
align-items: center;
gap: 24rpx;
.item {
display: flex;
align-items: center;
gap: 12rpx;
.icon,
.icon-active {
width: 36rpx;
height: 36rpx;
}
.icon-active {
display: none;
}
.i-content {
font-size: 28rpx;
color: rgba(156, 163, 175, 1);
}
&.active {
.icon {
display: none;
}
.icon-active {
display: block;
}
.i-content {
color: rgba(254, 181, 74, 1);
}
}
}
}
}
}
.page-footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: #fff;
border-radius: 40rpx 40rpx 0 0;
box-sizing: border-box;
padding: 32rpx 32rpx calc(env(safe-area-inset-bottom) + 32rpx);
.title {
font-size: 36rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.rate-wrap {
margin-top: 19rpx;
display: flex;
align-items: center;
justify-content: space-between;
.rate {
display: flex;
align-items: center;
gap: 36rpx;
.num {
font-size: 32rpx;
color: rgba(254, 181, 74, 1);
}
}
.r-status {
font-size: 28rpx;
color: rgba(254, 181, 74, 1);
}
}
.freetext {
margin-top: 48rpx;
display: flex;
gap: 16rpx;
.txa {
padding: 26rpx 32rpx;
height: 96rpx;
border-radius: 16rpx;
background-color: rgba(247, 248, 250, 1);
box-sizing: border-box;
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
max-height: 400rpx;
}
.txa-place {
color: rgba(148, 163, 184, 1);
}
.btn {
flex-shrink: 0;
width: 130rpx;
height: 96rpx;
font-size: 32rpx;
color: rgba(255, 255, 255, 1);
display: flex;
align-items: center;
justify-content: center;
background: rgba(74, 184, 253, 1);
box-shadow: 0rpx 15rpx 30rpx -6rpx rgba(0, 96, 143, 0.09);
border-radius: 16rpx 16rpx 16rpx 16rpx;
}
}
}
}

8
src/pages/agentEva/index.ts

@ -0,0 +1,8 @@
const _app = getApp<IAppOption>();
Page({
data: {},
onLoad() {},
});
export {}

77
src/pages/agentEva/index.wxml

@ -0,0 +1,77 @@
<view class="page">
<view class="page-header">
<view class="logo">
<image class="logo" src="/images/icon2.png"></image>
</view>
<view class="title">应用包生成助手</view>
<view class="content">智能应用包构建工具 · 版本 2.4.0</view>
<view class="rate-wrap">
<view class="num">3.5</view>
<view class="wrap">
<van-rate
value="{{4.5}}"
color="#F7B550"
allow-half
readonly
size="54rpx"
void-color="#F7F8FA"
void-icon="star"
/>
<view class="stat">共 7 条用户评价</view>
</view>
</view>
</view>
<view class="list">
<view class="list-title">全部评价</view>
<view class="list-card">
<view class="user">
<image class="avatar" src="/images/icon2.png"></image>
<view class="wrap">
<view class="w-header">
<view class="name">张同学</view>
<view class="idenity">学生</view>
<view class="idenity teacher">教职工</view>
</view>
<view class="date">信息工程学院 · 2小时前</view>
</view>
<view class="rate-num">3.5</view>
</view>
<view class="content">
这个生成助手真的帮了大忙!界面直观,配置流程非常清晰,大大缩短了我们项目部署的时间。建议以后可以增加更多自定义模板。
</view>
<view class="l-footer">
<view class="item">
<image class="icon" src="/images/icon52.png"></image>
<image class="icon-active" src="/images/icon53.png"></image>
<view class="i-content">34</view>
</view>
<view class="item">
<image class="icon" src="/images/icon54.png"></image>
<view class="i-content">回复</view>
</view>
</view>
</view>
</view>
<view class="page-footer">
<view class="title">评分</view>
<view class="rate-wrap">
<view class="rate">
<van-rate
value="{{4.5}}"
color="#F7B550"
allow-half
readonly
size="54rpx"
void-color="#F7F8FA"
void-icon="star"
/>
<view class="num">3.5</view>
</view>
<view class="r-status">非常满意</view>
</view>
<view class="freetext">
<textarea class="txa" placeholder="写下你的评价..." auto-height maxlength="-1" placeholder-class="txa-place"></textarea>
<view class="btn">发布</view>
</view>
</view>
</view>

7
src/pages/buses/index.json

@ -0,0 +1,7 @@
{
"navigationBarTitleText": "校园巴士",
"navigationStyle": "default",
"usingComponents": {
"van-icon": "@vant/weapp/icon/index"
}
}

314
src/pages/buses/index.scss

@ -0,0 +1,314 @@
page {
background-color: rgba(247, 248, 250, 1);
}
.page {
padding-bottom: calc(120px + env(safe-area-inset-bottom));
.map {
position: relative;
width: 100%;
height: 584rpx;
.mark {
.mark-container {
padding: 16rpx;
border-radius: 16rpx;
background-color: #fff;
display: flex;
.icon {
width: 36rpx;
height: 36rpx;
}
.wrap {
padding-left: 10rpx;
.title {
font-size: 28rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.content {
margin-top: 10rpx;
font-size: 22rpx;
color: rgba(17, 24, 39, 1);
}
}
}
.tril {
margin: -10rpx auto 0;
display: block;
width: 0;
height: 0;
border-style: solid;
border-width: 26rpx 26rpx 0 26rpx;
border-color: #fff transparent transparent transparent;
}
}
.to-center {
position: absolute;
top: 337rpx;
right: 36rpx;
width: 64rpx;
height: 64rpx;
background: #ffffff;
box-shadow: 0rpx 15rpx 30rpx 0rpx rgba(0, 96, 143, 0.09);
border-radius: 12rpx 12rpx 12rpx 12rpx;
display: flex;
align-items: center;
justify-content: center;
.icon {
width: 34rpx;
height: 34rpx;
}
}
}
.banner {
position: relative;
margin: -140rpx 30rpx 0;
padding: 32rpx;
border-radius: 24rpx;
background-color: #fff;
z-index: 1;
.b-header {
display: flex;
align-items: center;
gap: 24rpx;
.icon1 {
flex-shrink: 0;
width: 88rpx;
height: 88rpx;
}
.wrap {
.w-header {
.name {
margin-right: 15rpx;
display: inline-flex;
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
gap: 10rpx;
align-items: center;
flex-wrap: wrap;
.icon2 {
width: 37rpx;
height: 21rpx;
}
}
}
.content {
margin-top: 8rpx;
font-size: 28rpx;
color: rgba(100, 116, 139, 1);
}
}
}
.tip {
margin-top: 32rpx;
padding: 16rpx 24rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: space-between;
background-color: rgba(254, 181, 74, 0.1);
gap: 16rpx;
.icon {
flex-shrink: 0;
width: 36rpx;
height: 36rpx;
}
.content {
flex: 1;
font-size: 24rpx;
color: rgba(254, 181, 74, 1);
}
.cross {
font-size: 20rpx;
color: rgba(254, 181, 74, 0.49);
}
}
}
.container {
margin: 24rpx 30rpx 0;
padding: 30rpx;
background-color: rgba(255, 255, 255, 1);
border-radius: 24rpx;
.next {
padding: 32rpx;
background-color: rgba(74, 184, 253, 0.1);
border-radius: 16rpx;
.n-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
.title {
font-size: 40rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
.high {
display: inline;
color: rgba(74, 184, 253, 1);
}
}
.tag {
padding: 14rpx 26rpx;
border: 1px solid rgba(74, 184, 253, 1);
border-radius: 40rpx;
font-size: 28rpx;
color: rgba(74, 184, 253, 1);
line-height: 32rpx;
.icon {
width: 23rpx;
height: 26rpx;
}
}
}
.content {
margin-top: 15rpx;
font-size: 28rpx;
color: rgba(100, 116, 139, 1);
}
}
.scroll {
margin: 0 -24rpx;
padding-top: 88rpx;
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
&::-webkit-scrollbar {
display: none;
}
.item {
flex-shrink: 0;
.top {
position: relative;
display: flex;
align-items: center;
justify-content: center;
.badge {
position: absolute;
top: -73rpx;
width: 56rpx;
height: 56rpx;
border-radius: 12rpx;
background: linear-gradient(90deg, #9ddffd 0%, #4ab8fd 100%);
display: none;
align-items: center;
justify-content: center;
box-shadow: 0px 11.2px 28px 0px rgba(0, 96, 143, 0.25);
.icon {
width: 38rpx;
height: 38rpx;
}
&::after {
position: absolute;
bottom: -8rpx;
left: 50%;
transform: translateX(-50%);
display: block;
content: '';
width: 0;
height: 0;
border-style: solid;
border-width: 10rpx 10rpx 0 10rpx;
border-color: #85ccfc transparent transparent transparent;
}
}
.line-left,
.line-right {
width: 43rpx;
height: 12rpx;
background-color: rgba(247, 248, 250, 1);
}
.center {
position: relative;
z-index: 1;
margin: 0 -14rpx;
border: 2px solid #fff;
width: 40rpx;
height: 40rpx;
box-sizing: border-box;
font-size: 24rpx;
color: rgba(255, 255, 255, 1);
border-radius: 50%;
background-color: rgba(203, 213, 225, 1);
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.sub-center {
position: relative;
z-index: 1;
margin: 8rpx -14rpx;
width: 24rpx;
height: 24rpx;
box-sizing: border-box;
border: 2px solid #fff;
border-radius: 50%;
background-color: rgba(203, 213, 225, 1);
}
}
.name {
margin: 20rpx auto 0;
writing-mode: vertical-lr;
font-size: 28rpx;
color: rgba(71, 85, 105, 1);
letter-spacing: 8rpx;
}
&.active {
.line-left,
.line-right {
background-color: rgba(74, 184, 253, 1);
}
.center {
background-color: rgba(74, 184, 253, 1);
}
.sub-center {
background-color: rgba(74, 184, 253, 1);
}
}
&.last-active {
.badge {
display: flex;
}
.line-right {
background-color: rgba(247, 248, 250, 1);
}
.name {
color: rgba(74, 184, 253, 1);
}
}
&:first-of-type {
.line-left {
opacity: 0;
}
}
&:last-of-type {
.line-right {
opacity: 0;
}
}
}
}
}
.footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
box-sizing: border-box;
padding: 30rpx 30rpx calc(env(safe-area-inset-bottom) + 30rpx);
background: rgba(255, 255, 255, 0.8);
box-shadow: 0rpx -4rpx 21rpx 0rpx rgba(0, 0, 0, 0.07);
border-radius: 0rpx 0rpx 0rpx 0rpx;
.btn {
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;
}
}
}

53
src/pages/buses/index.ts

@ -0,0 +1,53 @@
const _app = getApp<IAppOption>()
const imagesUrl = _app.globalData.imageUrl
Page({
data: {
centerLat: 22.586521, // 纬度
centerLng: 113.930801, // 经度
markers: [
{
id: 1,
latitude: 22.586521,
longitude: 113.930801,
iconPath: `${imagesUrl}/icon64.png`, // 蓝圈白边站点图标
width: 14,
height: 14,
anchor: { x: 1, y: 1 },
label: '1111111',
customCallout: {
display: 'ALWAYS', // 永久显示气泡,不用点击
anchorY: -4,
},
collision: 'marker',
},
{
id: 2,
latitude: 22.58893,
longitude: 113.92762,
iconPath: `${imagesUrl}/icon64.png`, // 蓝圈白边站点图标
anchor: { x: 1, y: 1 },
width: 14,
height: 14,
customCallout: {
display: 'ALWAYS', // 永久显示气泡,不用点击
anchorY: -4,
},
collision: 'marker',
},
],
polyline: [
{
points: [
{ latitude: 22.586521, longitude: 113.930801 },
{ latitude: 22.58893, longitude: 113.92762 },
],
color: 'rgba(74, 184, 253, 1)',
width: 6,
},
],
},
onLoad() {},
})
export {}

83
src/pages/buses/index.wxml

@ -0,0 +1,83 @@
<view class="page">
<map
class="map"
latitude="{{centerLat}}"
longitude="{{centerLng}}"
scale="14"
markers="{{markers}}"
polyline="{{polyline}}"
enable-overlooking
enable-zoom
enable-scroll
enable-rotate
>
<view class="to-center">
<image class="icon" src="/images/icon2.png"></image>
</view>
<cover-view slot="callout">
<cover-view class="mark" marker-id="{{index+1}}" wx:for="{{2}}" wx:key="index">
<cover-view class="mark-container">
<cover-image class="icon" src="/images/icon46.png"></cover-image>
<cover-view class="wrap">
<cover-view class="title">留仙洞园区</cover-view>
<cover-view class="content">留仙洞园区体育馆·生公寓 距离下一站2分钟</cover-view>
</cover-view>
</cover-view>
<cover-view class="tril"></cover-view>
</cover-view>
</cover-view>
</map>
<view class="banner">
<view class="b-header">
<image class="icon1" src="/images/icon46.png"></image>
<view class="wrap">
<view class="w-header">
<view class="name">
西丽湖园区
<image class="icon2" src="/images/icon65.png"></image>
留仙洞园区
</view>
</view>
<view class="content">首 07:00 末 20:00</view>
</view>
</view>
<view class="tip">
<image class="icon" src="/images/icon66.png"></image>
<view class="content">点击当前所在站点或点击定位即可查看到站信息</view>
</view>
</view>
<view class="container">
<view class="next">
<view class="n-header">
<view class="title">
预计
<view class="high">11:55</view>
到站
</view>
<view class="tag">
<image class="icon" src="/images/icon67.png"></image>
发车时刻表
</view>
</view>
<view class="content">下一站:官龙山园区报告厅</view>
</view>
<view class="scroll">
<view class="item {{index<=4 && 'active'}} {{index==4 && 'last-active'}}" wx:for="{{10}}" wx:key="index">
<view class="top">
<view class="badge">
<image class="icon" src="/images/icon68.png"></image>
</view>
<view class="line-left"></view>
<view wx:if="{{index==0}}" class="center">始</view>
<view wx:elif="{{index==9}}" class="center">终</view>
<view wx:else class="sub-center"></view>
<view class="line-right"></view>
</view>
<view class="name">留仙洞园区体育馆</view>
</view>
</view>
</view>
<view class="footer">
<view class="btn">校园地图</view>
</view>
</view>

7
src/pages/chat/index.json

@ -0,0 +1,7 @@
{
"navigationBarTitleText": "智能助手",
"usingComponents": {
"van-icon": "@vant/weapp/icon/index"
}
}

160
src/pages/chat/index.scss

@ -0,0 +1,160 @@
page {
background-color: rgba(247, 248, 250, 1);
}
.page-back {
font-size: 32rpx;
color: rgba(0, 0, 0, 0.9);
}
.chat-page {
width: 100%;
height: 100vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
position: relative;
}
/* ========== 聊天消息列表 ========== */
.chat-scroll {
flex: 1;
position: relative;
padding: 160rpx 28rpx 0;
box-sizing: border-box;
box-sizing: border-box;
.history {
width: 167rpx;
height: 56rpx;
background: linear-gradient(90deg, #9ddffd 0%, #4ab8fd 100%);
font-size: 28rpx;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
border-radius: 54rpx;
}
.first-card {
margin-top: 39rpx;
padding: 32rpx;
font-size: 32rpx;
color: rgba(31, 41, 55, 1);
border-radius: 24rpx;
line-height: 56rpx;
background: linear-gradient(180deg, rgba(241, 249, 255, 0.84) 0%, #ffffff 100%);
border: 1px solid rgba(255, 255, 255, 1);
backdrop-filter: blur(38px);
box-shadow: 0px 15px 30px -4px rgba(0, 96, 143, 0.09);
.high {
display: inline;
color: rgba(74, 184, 253, 1);
}
}
.tip {
margin-top: 42rpx;
font-size: 28rpx;
color: rgba(203, 213, 225, 1);
text-align: center;
}
.chat-messages {
clear: both;
display: flow-root;
.user-message {
clear: both;
float: right;
margin-left: 80rpx;
margin-top: 42rpx;
padding: 24rpx 32rpx;
background: rgba(74, 184, 253, 1);
border-radius: 24rpx;
font-size: 32rpx;
color: #fff;
line-height: 56rpx;
}
.ai-message {
clear: both;
float: left;
margin-right: 80rpx;
margin-top: 42rpx;
padding: 24rpx 32rpx;
font-size: 32rpx;
line-height: 56rpx;
color: rgba(71, 85, 105, 1);
background-color: #fff;
border-radius: 24rpx;
.high {
display: inline;
color: rgba(74, 184, 253, 1);
}
}
}
.scroll-bottom-safe {
clear: both;
height: 223rpx;
}
}
.chat-input {
position: fixed;
left: 0;
width: calc(100% - 120rpx);
bottom: 83rpx;
margin: 0 30rpx;
padding: 0 18rpx;
height: 96rpx;
border-radius: 95rpx;
background-color: #fff;
box-shadow: 0px 15px 30px 0px rgba(0, 96, 143, 0.09);
.freetext {
padding-left: 26rpx;
height: 100%;
display: flex;
align-items: center;
.input {
flex: 1;
}
.icon {
padding-left: 26rpx;
flex-shrink: 0;
width: 60rpx;
height: 60rpx;
}
}
.voice {
height: 100%;
display: flex;
align-items: center;
.content {
flex: 1;
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
text-align: center;
}
.icon {
padding-left: 26rpx;
flex-shrink: 0;
width: 60rpx;
height: 60rpx;
}
}
.voiceing {
padding: 213rpx 0 0;
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 448rpx;
box-sizing: border-box;
.tip {
font-size: 32rpx;
color: #fff;
text-align: center;
}
.ani {
display: block;
margin: 23rpx auto 0;
width: 400rpx;
height: 86rpx;
}
}
}

251
src/pages/chat/index.ts

@ -0,0 +1,251 @@
/**
* Chat
*/
interface Message {
id: string
role: 'user' | 'ai'
content: string
timestamp: number
}
Page({
data: {
/** 消息列表 */
messages: [] as Message[],
/** 输入文本 */
inputText: '',
/** 输入模式:text 或 voice */
inputMode: 'voice' as 'text' | 'voice',
/** 是否正在录音 */
isRecording: false,
/** 录音提示文本 */
recordingTip: '松手发送,上移取消',
/** 录音管理器 */
recorderManager: null as WechatMiniprogram.RecorderManager | null,
/** 滚动到指定消息 */
scrollToView: '',
/** 页面顶部距离 */
pageTop: 0,
/** 导航栏背景 */
background: 'transparent',
},
onLoad() {
// 初始化录音管理器
const recorderManager = wx.getRecorderManager()
recorderManager.onStart(() => {
this.setData({ isRecording: true, recordingTip: '松手发送,上移取消' })
})
recorderManager.onStop((res) => {
this.setData({ isRecording: false })
if (res.duration < 1000) {
wx.showToast({ title: '录音时间太短', icon: 'none' })
return
}
// 发送语音消息
this.sendVoiceMessage(res.tempFilePath, res.duration)
})
recorderManager.onError((err) => {
this.setData({ isRecording: false })
wx.showToast({ title: '录音失败', icon: 'none' })
console.error('录音错误:', err)
})
this.setData({ recorderManager })
// 获取系统信息设置页面顶部距离
const systemInfo = wx.getSystemInfoSync()
this.setData({ pageTop: systemInfo.statusBarHeight + 44 })
},
/**
*
*/
switchToVoice() {
// 如果 input 有内容,不允许切换到语音模式
if (this.data.inputText.trim()) {
wx.showToast({ title: '请先发送或清空内容', icon: 'none' })
return
}
this.setData({ inputMode: 'voice' })
},
/**
*
*/
switchToText() {
this.setData({ inputMode: 'text' })
},
/**
*
*/
onInputChange(e: WechatMiniprogram.InputEvent) {
this.setData({ inputText: e.detail.value })
},
/**
*
*/
onSendText() {
const { inputText } = this.data
if (!inputText.trim()) return
// 添加用户消息
const userMessage: Message = {
id: `user_${Date.now()}`,
role: 'user',
content: inputText.trim(),
timestamp: Date.now(),
}
this.setData({
messages: [...this.data.messages, userMessage],
inputText: '',
})
// 模拟 AI 回复
this.simulateAIResponse()
},
/**
* chat-input
*/
onChatInputLongPress() {
// 文本模式下有内容时,不允许录音
if (this.data.inputMode === 'text' && this.data.inputText.trim()) {
wx.showToast({ title: '请先发送或清空内容', icon: 'none' })
return
}
const { recorderManager } = this.data
if (!recorderManager) return
recorderManager.start({
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 48000,
format: 'mp3',
})
},
/**
* chat-input
*/
onChatInputStart() {
// 用于检测长按,不做实际操作
// 长按逻辑由 bindlongpress 处理
},
/**
* chat-input
*/
onChatInputEnd() {
const { recorderManager, isRecording } = this.data
if (!recorderManager || !isRecording) return
recorderManager.stop()
},
/**
* chat-input
*/
onChatInputCancel() {
const { recorderManager, isRecording } = this.data
if (!recorderManager || !isRecording) return
recorderManager.stop()
this.setData({ isRecording: false, recordingTip: '录音已取消' })
wx.showToast({ title: '录音已取消', icon: 'none' })
},
/**
* voiceing
*/
onVoiceingTouchMove(e: WechatMiniprogram.TouchEvent) {
const { isRecording, recordingTip } = this.data
if (!isRecording) return
// 获取触摸点位置
const touch = e.touches[0]
const systemInfo = wx.getSystemInfoSync()
const screenHeight = systemInfo.screenHeight
// voiceing 区域高度为 448rpx,约 224px
// 当手指 Y 坐标小于 (屏幕高度 - voiceing高度) 时,说明手指移出了区域
const voiceingHeight = 224 // 448rpx ≈ 224px
const threshold = screenHeight - voiceingHeight
if (touch.clientY < threshold) {
// 手指移出区域,准备取消
if (recordingTip !== '上移取消录音') {
this.setData({ recordingTip: '上移取消录音' })
}
} else {
// 手指在区域内,恢复提示
if (recordingTip !== '松手发送,上移取消') {
this.setData({ recordingTip: '松手发送,上移取消' })
}
}
},
/**
* voiceing
*/
onVoiceingTouchEnd(e: WechatMiniprogram.TouchEvent) {
const { recorderManager, isRecording, recordingTip } = this.data
if (!recorderManager || !isRecording) return
// 获取触摸点位置
const changedTouch = e.changedTouches[0]
const systemInfo = wx.getSystemInfoSync()
const screenHeight = systemInfo.screenHeight
const voiceingHeight = 224 // 448rpx ≈ 224px
const threshold = screenHeight - voiceingHeight
// 如果手指移出区域,取消录音
if (changedTouch.clientY < threshold || recordingTip === '上移取消录音') {
recorderManager.stop()
this.setData({ isRecording: false, recordingTip: '录音已取消' })
wx.showToast({ title: '录音已取消', icon: 'none' })
} else {
// 手指在区域内,正常结束录音并发送
recorderManager.stop()
}
},
/**
*
*/
sendVoiceMessage(filePath: string, duration: number) {
const userMessage: Message = {
id: `user_${Date.now()}`,
role: 'user',
content: `[语音 ${Math.ceil(duration / 1000)}秒]`,
timestamp: Date.now(),
}
this.setData({
messages: [...this.data.messages, userMessage],
})
// 模拟 AI 回复
this.simulateAIResponse()
},
/**
* AI
*/
simulateAIResponse() {
setTimeout(() => {
const aiMessage: Message = {
id: `ai_${Date.now()}`,
role: 'ai',
content: '好的,我已经收到您的消息,正在为您处理中...',
timestamp: Date.now(),
}
this.setData({
messages: [...this.data.messages, aiMessage],
})
}, 1000)
},
})
export {}

69
src/pages/chat/index.wxml

@ -0,0 +1,69 @@
<view
class="chat-page"
style="background: url('/images/bg3.png') no-repeat top center/100% 556rpx;padding-top: {{pageTop}}px;"
>
<navbar fixed customStyle="background:{{background}};">
<van-icon class="page-back" name="arrow-left" slot="left" />
</navbar>
<scroll-view class="chat-scroll" scroll-y scroll-into-view="{{scrollToView}}" enhanced show-scrollbar="{{false}}">
<view class="history">最近对话</view>
<view class="first-card">
你好呀~我是你的活动创建小助手你可以直接告诉我想创建什么活动,比如
<view class="high">“下周五下午3点在报告厅办一场心理健康讲座”</view>
我会帮你自动整理活动信息。
</view>
<view class="tip">特别说明:内容均为ai生成,仅供参考</view>
<view class="chat-messages">
<block wx:for="{{4}}" wx:key="index">
<view class="user-message">请帮我创建一个下周活动,活动主题是新生音乐会,活动地点是学校音乐厅</view>
<view class="ai-message">
信息我已经整理好~我还需要再确认一点信息:
<view class="high">您的活动打算什么时候开始呢?告诉我开始和结束的时间,我来帮您创建~</view>
</view>
</block>
</view>
<view class="scroll-bottom-safe"></view>
</scroll-view>
<view
class="chat-input"
bindtouchstart="onChatInputStart"
bindtouchend="onChatInputEnd"
bindtouchcancel="onChatInputCancel"
bindlongpress="onChatInputLongPress"
>
<!-- 文本输入模式 -->
<view class="freetext" wx:if="{{inputMode === 'text'}}">
<input
class="input"
placeholder-class="input-place"
type="text"
placeholder="发消息或按住说话..."
value="{{inputText}}"
disabled="{{isRecording}}"
bindinput="onInputChange"
bindconfirm="onSendText"
/>
<!-- 切换到语音模式按钮 -->
<image class="icon" src="/images/icon44.png" bindtap="switchToVoice" catchtap></image>
</view>
<!-- 语音输入模式(未录音状态) -->
<view class="voice" wx:if="{{inputMode === 'voice' && !isRecording}}">
<view class="content">按住说话</view>
<!-- 切换到文本模式按钮 -->
<image class="icon" src="/images/icon45.png" bindtap="switchToText" catchtap></image>
</view>
<!-- 录音进行中状态 -->
<view
class="voiceing"
wx:if="{{isRecording}}"
style="background: url('/images/bg4.png') no-repeat top center/100%"
bindtouchmove="onVoiceingTouchMove"
bindtouchend="onVoiceingTouchEnd"
>
<view class="tip">{{recordingTip}}</view>
<image class="ani" src="/images/gif1.gif"></image>
</view>
</view>
</view>

5
src/pages/chatHistory/index.json

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "最近对话",
"navigationStyle": "default",
"usingComponents": {}
}

29
src/pages/chatHistory/index.scss

@ -0,0 +1,29 @@
page {
background-color: rgba(247, 248, 250, 1);
}
.page {
padding: 30rpx;
.card {
padding: 32rpx;
background-color: #fff;
border-radius: 24rpx;
display: flex;
gap: 24rpx;
.wrap {
.title {
font-size: 36rpx;
color: rgba(17, 24, 39, 1);
line-height: 56rpx;
}
.date{
margin-top: 14rpx;
font-size: 32rpx;
color: rgba(148, 163, 184, 1);
}
}
.status{
font-size: 32rpx;
color: rgba(203, 213, 225, 1);
}
}
}

8
src/pages/chatHistory/index.ts

@ -0,0 +1,8 @@
const _app = getApp<IAppOption>();
Page({
data: {},
onLoad() {},
});
export {}

9
src/pages/chatHistory/index.wxml

@ -0,0 +1,9 @@
<view class="page">
<view class="card">
<view class="wrap">
<view class="title">新生音乐会</view>
<view class="date">2026/6/15</view>
</view>
<view class="status">草稿</view>
</view>
</view>

1
src/pages/index/index.wxml

@ -2,6 +2,7 @@
class="page" class="page"
style="background: url('/images/bg1.png') no-repeat top center/100% 655rpx;padding-top: {{pageTop}}px;" style="background: url('/images/bg1.png') no-repeat top center/100% 655rpx;padding-top: {{pageTop}}px;"
> >
<view class="search"> <view class="search">
<image class="icon" src="/images/icon1.png"></image> <image class="icon" src="/images/icon1.png"></image>
<view class="content">请搜索你想要的内容</view> <view class="content">请搜索你想要的内容</view>

77
src/pages/login/index.scss

@ -1,3 +1,78 @@
.page { .page {
padding: 20rpx; padding: 0 30rpx;
.page-title {
padding-top: 70rpx;
font-size: 64rpx;
line-height: 72rpx;
color: rgba(71, 85, 105, 1);
font-weight: bold;
.t1 {
color: rgba(71, 85, 105, 1);
}
.t2 {
color: rgba(74, 184, 253, 1);
}
}
.page-body {
margin-top: 74rpx;
padding: 30rpx;
border-radius: 16rpx;
background-color: #fff;
.tip {
padding: 24rpx;
background-color: rgba(254, 181, 74, 0.1);
border-radius: 16rpx;
display: flex;
gap: 18rpx;
.icon {
flex-shrink: 0;
margin-top: 8rpx;
width: 32rpx;
height: 32rpx;
}
.content {
font-size: 28rpx;
color: rgba(254, 181, 74, 1);
}
}
.form-item {
margin-top: 32rpx;
display: flex;
align-items: center;
gap: 32rpx;
padding: 0 30rpx;
background-color: rgba(248, 250, 253, 1);
border-radius: 16rpx;
.label {
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
&.required::before {
display: inline;
content: '*';
color: rgba(253, 91, 89, 1);
}
}
.input {
height: 84rpx;
font-size: 32rpx;
color: rgba(71, 85, 105, 1);
}
.input-place {
color: rgba(203, 213, 225, 1);
}
}
.tip2{
margin-top: 28rpx;
font-size: 28rpx;
color: rgba(148, 163, 184, 1);
line-height: 42rpx;
}
.agreement{
font-size: 28rpx;
color: rgba(71, 85, 105, 1);
.high{
color: rgba(74, 184, 253, 1);
}
}
}
} }

31
src/pages/login/index.wxml

@ -1,3 +1,30 @@
<view class="page"> <view
<text>登录页</text> class="page"
style="background: url('/images/bg6.png') no-repeat top center/100% 556rpx;padding-top: {{pageTop}}px;"
>
<view class="page-title">
<view class="t1">绑定</view>
<view class="t2">深职大账号</view>
</view>
<view class="page-body">
<view class="tip">
<image class="icon" src="/images/icon26.png"></image>
<view class="content">
请确保您输入的密码是正确的,错误次数超过2次SIC将冻结您的账号一小时。超过一定错误次数,学校系统将冻结您的账号。忘记密码请前往
https://authserver.szpu.edu.cn进行重置
</view>
</view>
<view class="form-item">
<view class="label required">账号</view>
<input class="input" placeholder-class="input-palce" type="number" placeholder="请输入学工号" />
</view>
<view class="form-item">
<view class="label required">密码</view>
<input class="input" placeholder-class="input-palce" type="text" placeholder="请输入密码" />
</view>
<view class="tip2">SIC+仅将您的密码用于单次身份验证,不会保存您的密码</view>
<view class="agreement">
<radio>我已阅读并接受《深职大SIC+小程序隐私保护指引》</radio>
</view>
</view>
</view> </view>

6
src/pages/my/index.json

@ -1,3 +1,7 @@
{ {
"usingComponents": {} "navigationStyle": "custom",
"navigationBarTitleText": "我的",
"usingComponents": {
"van-icon": "@vant/weapp/icon/index"
}
} }

95
src/pages/my/index.scss

@ -1,3 +1,96 @@
page {
background-color: rgba(247, 248, 250, 1);
}
.page { .page {
padding: 20rpx; padding: 0 32rpx;
.user {
padding-top: 50rpx;
display: flex;
gap: 24rpx;
.avatar {
width: 116rpx;
height: 116rpx;
border-radius: 50%;
border: 1px solid #fff;
.a-img {
display: block;
width: 100%;
height: 100%;
}
}
.wrap {
padding-top: 10rpx;
padding-left: 24rpx;
.w-header {
display: flex;
align-items: baseline;
gap: 20rpx;
.name {
font-size: 36rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.id {
font-size: 32rpx;
}
}
.content {
margin-top: 16rpx;
font-size: 26rpx;
color: rgba(148, 163, 184, 1);
}
}
}
.kkd {
margin-top: 42rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 22rpx;
.k-item {
flex: 1;
background: linear-gradient(180deg, #ebfdff 0%, #ffffff 100%);
border-radius: 24rpx 24rpx 24rpx 24rpx;
border: 2rpx solid #ffffff;
.wrap {
padding: 32rpx;
.title {
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.content {
margin-top: 16rpx;
font-size: 28rpx;
color: rgba(148, 163, 184, 1);
}
}
}
}
.list {
margin-top: 30rpx;
background-color: #fff;
border-radius: 24rpx;
padding: 0 32rpx;
.list-item {
padding: 32rpx 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18rpx;
font-size: 28rpx;
color: rgba(203, 213, 225, 1);
border-bottom: 1px solid rgba(203, 213, 225, 0.21);
.icon {
flex-shrink: 0;
width: 42rpx;
height: 42rpx;
}
.name {
flex: 1;
font-size: 28rpx;
color: rgba(17, 24, 39, 1);
}
}
}
} }

15
src/pages/my/index.ts

@ -12,6 +12,21 @@ Page({
userInfo: app.globalData.userInfo, userInfo: app.globalData.userInfo,
}) })
}, },
handleMyAct() {
wx.navigateTo({
url: '/pages/myAct/index',
})
},
handleMyAgent() {
wx.navigateTo({
url: '/pages/myAgent/index',
})
},
handleMyCommet() {
wx.navigateTo({
url: '/pages/myComment/index',
})
},
}) })
export {} export {}

49
src/pages/my/index.wxml

@ -1,3 +1,48 @@
<view class="page"> <view
<text>我的</text> class="page"
style="background: url('/images/bg5.png') no-repeat top center/100% 655rpx;padding-top: {{pageTop}}px;"
>
<view class="user">
<view class="avatar">
<image class="a-img" src="/images/icon2.png"></image>
</view>
<view class="wrap">
<view class="w-header">
<view class="name">奇妙的双子座</view>
<view class="id">20989182</view>
</view>
<view class="content">计算机科学学院软件工程3班</view>
</view>
</view>
<view class="kkd">
<view class="k-item" bind:tap="handleMyAct">
<view class="wrap" style="background: url('/images/icon58.png') no-repeat top 24rpx right 24rpx/84rpx 84rpx">
<view class="title">我的活动</view>
<view class="content">已参与10个活动</view>
</view>
</view>
<view class="k-item" bind:tap="handleMyAgent">
<view class="wrap" style="background: url('/images/icon59.png') no-repeat top 24rpx right 24rpx/84rpx 84rpx">
<view class="title">我的智能体</view>
<view class="content">已启用20个智能体</view>
</view>
</view>
</view>
<view class="list">
<view class="list-item" bind:tap="handleMyCommet">
<image class="icon" src="/images/icon59.png"></image>
<view class="name">我的评论</view>
<van-icon name="arrow" />
</view>
<view class="list-item">
<image class="icon" src="/images/icon60.png"></image>
<view class="name">我的收藏</view>
<van-icon name="arrow" />
</view>
<view class="list-item">
<image class="icon" src="/images/icon61.png"></image>
<view class="name">我的评论</view>
<van-icon name="arrow" />
</view>
</view>
</view> </view>

9
src/pages/myAct/index.json

@ -0,0 +1,9 @@
{
"navigationBarTitleText": "我的活动",
"navigationStyle": "default",
"usingComponents": {
"van-tab": "@vant/weapp/tab/index",
"van-tabs": "@vant/weapp/tabs/index",
"van-icon": "@vant/weapp/icon/index"
}
}

141
src/pages/myAct/index.scss

@ -0,0 +1,141 @@
page {
background-color: rgba(247, 248, 250, 1);
}
.page {
.tabs {
.van-tabs__line {
width: 42rpx !important;
}
.van-tab--active {
--tab-font-size: 32rpx;
}
}
.page0 {
padding: 43rpx 30rpx;
.card {
margin-bottom: 24rpx;
padding: 24rpx;
background-color: #fff;
border-radius: 24rpx;
.c-body {
display: flex;
gap: 24rpx;
padding-bottom: 24rpx;
border-bottom: 1px solid rgba(247, 248, 250, 1);
&:last-of-type {
border: none;
}
.photo {
flex-shrink: 0;
position: relative;
width: 262rpx;
height: 196rpx;
border-radius: 16rpx;
overflow: hidden;
.status {
position: absolute;
top: 0;
left: 0;
padding: 8rpx 16rpx;
font-size: 22rpx;
border-radius: 16rpx 0rpx 16rpx 0rpx;
color: rgba(255, 255, 255, 1);
&.status1 {
background: #feb54a;
}
&.status2 {
background: rgba(74, 184, 253, 1);
}
&.status3 {
background: rgba(111, 220, 174, 1);
}
&.status4 {
background: rgba(203, 213, 225, 1);
}
}
.p-img {
border-radius: 16rpx;
display: block;
width: 100%;
height: 100%;
}
.user {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
box-sizing: border-box;
text-align: center;
line-height: 44rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 1);
background-color: rgba(0, 0, 0, 0.36);
backdrop-filter: blur(8rpx);
}
}
.wrap {
display: flex;
flex-direction: column;
justify-content: space-between;
.title {
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
line-height: 48rpx;
font-weight: bold;
height: 96rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.date,
.site {
margin-top: 12rpx;
font-size: 28rpx;
color: rgba(100, 116, 139, 1);
display: flex;
align-items: center;
gap: 8rpx;
.icon {
width: 22rpx;
height: 22rpx;
}
}
}
}
.c-footer {
padding-top: 24rpx;
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 16rpx;
.stat {
flex-grow: 1;
flex-shrink: 0;
font-size: 28rpx;
color: rgba(148, 163, 184, 1);
}
.btn1 {
padding: 12rpx 32rpx;
border: 1px solid rgba(74, 184, 253, 1);
border-radius: 50rpx;
font-size: 28rpx;
line-height: 32rpx;
color: rgba(74, 184, 253, 1);
}
.btn2 {
padding: 12rpx 32rpx;
border: 1px solid rgba(74, 184, 253, 1);
border-radius: 50rpx;
font-size: 28rpx;
line-height: 32rpx;
color: #fff;
background-color: rgba(74, 184, 253, 1);
}
}
}
}
}

8
src/pages/myAct/index.ts

@ -0,0 +1,8 @@
const _app = getApp<IAppOption>();
Page({
data: {},
onLoad() {},
});
export {}

49
src/pages/myAct/index.wxml

@ -0,0 +1,49 @@
<view class="page">
<van-tabs
class="tabs"
color="rgba(74, 184, 253, 1)"
title-inactive-color="rgba(71, 85, 105, 1)"
title-active-color="rgba(74, 184, 253, 1)"
active="{{ active }}"
bind:change="onChange"
>
<van-tab title="我发布的">
<view class="page0">
<view class="card" wx:for="{{10}}" wx:key="index">
<view class="c-body">
<view class="photo">
<view class="status status1">进行中</view>
<view class="status status2">已发布</view>
<view class="status status3">报名中</view>
<view class="status status4">草稿</view>
<view class="status status4">已取消</view>
<view class="status status4">已结束</view>
<image class="p-img" src="/images/bg1.png"></image>
<view class="user">128人已报名</view>
</view>
<view class="wrap">
<view class="title">深职大第十五届校园歌手大赛</view>
<view class="date">
<image class="icon" src="/images/icon3.png"></image>
<view class="content">2026.04.01-2026.05.30</view>
</view>
<view class="site">
<image class="icon" src="/images/icon15.png"></image>
<view class="content">留仙洞校区音乐厅</view>
</view>
</view>
</view>
<view class="c-footer">
<view class="stat">86人已报名</view>
<view class="btn1">删除</view>
<!-- <view class="btn1">签到二维码</view> -->
<!-- <view class="btn1">取消</view> -->
<!-- <view class="btn1">详情</view> -->
<view class="btn2">编辑</view>
</view>
</view>
</view>
</van-tab>
<van-tab title="我参与的">内容 2</van-tab>
</van-tabs>
</view>

9
src/pages/myAgent/index.json

@ -0,0 +1,9 @@
{
"navigationBarTitleText": "我的智能体",
"navigationStyle": "default",
"usingComponents": {
"van-tab": "@vant/weapp/tab/index",
"van-tabs": "@vant/weapp/tabs/index",
"van-icon": "@vant/weapp/icon/index"
}
}

95
src/pages/myAgent/index.scss

@ -0,0 +1,95 @@
page {
background-color: rgba(247, 248, 250, 1);
}
.page {
.tabs {
.van-tabs__line {
width: 42rpx !important;
}
.van-tab--active {
--tab-font-size: 32rpx;
}
}
.page0 {
padding: 43rpx 30rpx;
.card {
margin-bottom: 24rpx;
padding: 24rpx;
background-color: #fff;
border-radius: 24rpx;
display: flex;
gap: 24rpx;
padding-bottom: 24rpx;
border-bottom: 1px solid rgba(247, 248, 250, 1);
&:last-of-type {
border: none;
}
.photo {
flex-shrink: 0;
position: relative;
width: 158rpx;
height: 158rpx;
border-radius: 16rpx;
overflow: hidden;
.p-img {
border-radius: 16rpx;
display: block;
width: 100%;
height: 100%;
}
}
.wrap {
flex: 1;
.title {
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
line-height: 48rpx;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.content {
margin-top: 12rpx;
font-size: 28rpx;
color: rgba(148, 163, 184, 1);
line-height: 36rpx;
}
.w-footer {
padding-top: 20rpx;
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 16rpx;
.stat {
flex-grow: 1;
flex-shrink: 0;
font-size: 28rpx;
color: rgba(148, 163, 184, 0.5);
}
.btn1 {
padding: 8rpx 32rpx;
border: 1px solid rgba(74, 184, 253, 1);
border-radius: 50rpx;
font-size: 28rpx;
line-height: 32rpx;
color: rgba(74, 184, 253, 1);
}
.btn2 {
padding: 8rpx 32rpx;
border: 1px solid rgba(74, 184, 253, 1);
border-radius: 50rpx;
font-size: 28rpx;
line-height: 32rpx;
color: #fff;
background-color: rgba(74, 184, 253, 1);
}
}
}
}
}
}

8
src/pages/myAgent/index.ts

@ -0,0 +1,8 @@
const _app = getApp<IAppOption>();
Page({
data: {},
onLoad() {},
});
export {}

31
src/pages/myAgent/index.wxml

@ -0,0 +1,31 @@
<view class="page">
<van-tabs
class="tabs"
color="rgba(74, 184, 253, 1)"
title-inactive-color="rgba(71, 85, 105, 1)"
title-active-color="rgba(74, 184, 253, 1)"
active="{{ active }}"
bind:change="onChange"
>
<van-tab title="我发布的">
<view class="page0">
<view class="card" wx:for="{{10}}" wx:key="index">
<view class="photo">
<image class="p-img" src="/images/bg1.png"></image>
</view>
<view class="wrap">
<view class="title">深职大第十五届校园歌手大赛</view>
<div class="content">智能规划课表,轻松掌握学习节奏</div>
<view class="w-footer">
<view class="stat">86人已报名</view>
<view class="btn1">取消收藏</view>
<view class="btn2">使用</view>
</view>
</view>
</view>
</view>
</van-tab>
<van-tab title="历史使用">内容 2</van-tab>
<van-tab title="我收藏的">内容 2</van-tab>
</van-tabs>
</view>

10
src/pages/myComment/index.json

@ -0,0 +1,10 @@
{
"navigationBarTitleText": "我的评论",
"navigationStyle": "default",
"usingComponents": {
"van-tab": "@vant/weapp/tab/index",
"van-tabs": "@vant/weapp/tabs/index",
"van-icon": "@vant/weapp/icon/index",
"van-rate": "@vant/weapp/rate/index"
}
}

167
src/pages/myComment/index.scss

@ -0,0 +1,167 @@
page {
background-color: rgba(247, 248, 250, 1);
}
.page {
.tabs {
.van-tabs__line {
width: 42rpx !important;
}
.van-tab--active {
--tab-font-size: 32rpx;
}
}
.page0 {
padding: 43rpx 30rpx;
.card {
padding: 32rpx;
background-color: #fff;
border-radius: 24rpx;
.c-header {
display: flex;
align-items: center;
justify-content: space-between;
.status {
padding: 8rpx 16rpx;
font-size: 28rpx;
line-height: 32rpx;
border-radius: 8rpx;
&.status1 {
color: rgba(74, 184, 253, 1);
background-color: rgba(74, 184, 253, 0.1);
}
&.status2 {
color: rgba(111, 220, 174, 1);
background-color: rgba(111, 220, 174, 0.1);
}
&.status3 {
color: rgba(253, 91, 89, 1);
background-color: rgba(253, 91, 89, 0.1);
}
}
.wrap {
display: flex;
align-items: center;
gap: 24rpx;
.name {
font-size: 28rpx;
color: rgba(148, 163, 184, 1);
}
.options {
.icon {
width: 32rpx;
height: 32rpx;
}
}
}
}
.station {
margin-top: 28rpx;
display: flex;
align-items: center;
justify-content: space-between;
.date {
font-size: 28rpx;
color: rgba(148, 163, 184, 1);
}
.rate {
display: flex;
align-items: center;
gap: 8rpx;
.num {
font-size: 28rpx;
color: rgba(254, 181, 74, 1);
}
}
}
.content {
margin-top: 32rpx;
font-size: 28rpx;
color: rgba(17, 24, 39, 1);
line-height: 46rpx;
}
.photo-wrap {
margin-top: 24rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
flex-wrap: wrap;
.photo {
aspect-ratio: 1 / 1;
.p-img {
display: block;
width: 100%;
height: 100%;
border-radius: 16rpx;
}
}
}
.act-card {
margin-top: 24rpx;
padding: 16rpx;
border-radius: 16rpx;
background-color: rgba(249, 250, 251, 1);
display: flex;
align-items: center;
gap: 24rpx;
.ac-photo {
flex-shrink: 0;
border-radius: 8rpx;
width: 80rpx;
height: 80rpx;
}
.wrap {
padding-top: 5rpx;
.title {
font-size: 32rpx;
color: rgba(17, 24, 39, 1);
font-weight: bold;
}
.content {
margin-top: 8rpx;
font-size: 24rpx;
color: rgba(148, 163, 184, 1);
}
}
}
.tip-card {
margin-top: 24rpx;
border-radius: 16rpx;
background: rgba(74, 184, 253, 0.05);
padding: 20rpx 16rpx;
display: flex;
gap: 16rpx;
.icon {
margin-top: 5rpx;
flex-shrink: 0;
width: 22rpx;
height: 22rpx;
}
.t-content {
font-size: 28rpx;
line-height: 32rpx;
color: rgba(74, 184, 253, 1);
}
}
.reject-card {
margin-top: 24rpx;
border-radius: 16rpx;
background: rgba(253, 91, 89, 0.1);
padding: 20rpx 16rpx;
display: flex;
gap: 16rpx;
.icon {
margin-top: 5rpx;
flex-shrink: 0;
width: 22rpx;
height: 22rpx;
}
.t-content {
font-size: 28rpx;
line-height: 32rpx;
color: rgba(253, 91, 89, 1);
}
}
}
}
}

8
src/pages/myComment/index.ts

@ -0,0 +1,8 @@
const _app = getApp<IAppOption>();
Page({
data: {},
onLoad() {},
});
export {}

58
src/pages/myComment/index.wxml

@ -0,0 +1,58 @@
<view class="page">
<van-tabs
class="tabs"
color="rgba(74, 184, 253, 1)"
title-inactive-color="rgba(71, 85, 105, 1)"
title-active-color="rgba(74, 184, 253, 1)"
active="{{ active }}"
bind:change="onChange"
>
<van-tab title="我发布的">
<view class="page0">
<view class="card">
<view class="c-header">
<view class="status status1">待审核</view>
<view class="wrap">
<view class="name">匿名</view>
<view class="options">
<image class="icon"></image>
</view>
</view>
</view>
<view class="station">
<view class="date">2026-01-01 13:00:09</view>
<view class="rate">
<van-rate value="{{4.5}}" color="#F7B550" allow-half size="28rpx" void-color="#F7F8FA" />
<view class="num">4.5</view>
</view>
</view>
<view class="content">
本次活动组织有序、流程顺畅,现场氛围良好。活 动内容丰富、安排合理,服务贴心到位,整体体验 良好。
</view>
<view class="photo-wrap">
<view class="photo" wx:for="{{3}}" wx:key="index">
<image class="p-img" src="/images/bg1.png" mode="aspectFill"></image>
</view>
</view>
<view class="act-card">
<image class="ac-photo" mode="aspectFill" src="/images/bg1.png"></image>
<view class="wrap">
<view class="title">深职大第十五届校园歌手大赛</view>
<view class="content">2026年5月30日 19:00</view>
</view>
</view>
<view class="tip-card">
<image class="icon" src="/images/icon62.png"></image>
<view class="t-content">内容已提交,正在审核中,预计1-2个工作日内完成审核。</view>
</view>
<view class="reject-card">
<image class="icon" src="/images/icon63.png"></image>
<view class="t-content">驳回原因:内容中包含不当言论,请修改后重新提交。</view>
</view>
</view>
</view>
</van-tab>
<van-tab title="历史使用">内容 2</van-tab>
<van-tab title="我收藏的">内容 2</van-tab>
</van-tabs>
</view>
Loading…
Cancel
Save