chore: initialize mini program frontend

This commit is contained in:
souplearn
2026-06-24 15:19:14 +08:00
commit fa73a3c184
96 changed files with 20561 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
const api = require('../../utils/api')
const emptyForm = {
id: '',
title: '',
content: '',
attendancePointNames: [],
sortOrder: 0,
isActive: true,
}
Page({
data: {
rows: [],
page: 1,
pageInput: '1',
totalPages: 1,
form: { ...emptyForm },
formVisible: false,
formTitle: '新增通知',
attendancePoints: [],
refreshing: false,
},
onShow() {
this.init()
},
async init() {
await this.loadAttendancePoints()
await this.load()
},
async loadAttendancePoints() {
try {
const attendancePoints = await api.listAccessibleAttendancePoints()
this.setData({ attendancePoints: attendancePoints || [] })
} catch (error) {
wx.showToast({ title: error.message || '加载考勤点失败', icon: 'none' })
}
},
async load(page = this.data.page) {
try {
const result = await api.listNotices({
includeInactive: true,
page,
pageSize: 8,
})
this.setData({
rows: result.rows,
page: result.page,
pageInput: String(result.page || 1),
totalPages: result.totalPages,
})
} catch (error) {
wx.showToast({ title: error.message, icon: 'none' })
}
},
async onPullDownRefresh() {
this.setData({ refreshing: true })
try {
await this.load(1)
} finally {
this.setData({ refreshing: false })
if (wx.stopPullDownRefresh) {
wx.stopPullDownRefresh()
}
}
},
noop() {},
checkedPointMap(names = []) {
const selected = new Set(names || [])
return this.data.attendancePoints.map(point => ({
...point,
checked: selected.has(point.name),
}))
},
defaultPointNames() {
return this.data.attendancePoints.map(point => point.name)
},
openCreate() {
const pointNames = this.defaultPointNames()
this.setData({
form: { ...emptyForm, attendancePointNames: pointNames },
attendancePoints: this.checkedPointMap(pointNames),
formTitle: '新增通知',
formVisible: true,
})
},
edit(e) {
const index = e.currentTarget.dataset.index
const notice = this.data.rows[index]
this.setData({
form: { ...notice },
attendancePoints: this.checkedPointMap(notice.attendancePointNames || []),
formTitle: '编辑通知',
formVisible: true,
})
},
closeForm() {
this.setData({
formVisible: false,
form: { ...emptyForm },
attendancePoints: this.checkedPointMap([]),
})
},
clearForm() {
this.setData({
form: { ...emptyForm },
attendancePoints: this.checkedPointMap([]),
})
},
onInput(e) {
const field = e.currentTarget.dataset.field
this.setData({
[`form.${field}`]: e.detail.value,
})
},
onActiveChange(e) {
this.setData({
'form.isActive': e.detail.value,
})
},
onPointCheckboxChange(e) {
const names = e.detail.value || []
this.setData({
'form.attendancePointNames': names,
attendancePoints: this.checkedPointMap(names),
})
},
async save() {
const form = {
...this.data.form,
sortOrder: Number(this.data.form.sortOrder || 0),
}
if (!(form.attendancePointNames || []).length) {
wx.showToast({ title: '请选择通知考勤点', icon: 'none' })
return
}
wx.showLoading({ title: '保存中' })
try {
await api.saveNotice(form)
wx.showToast({ title: '已保存', icon: 'success' })
this.setData({
form: { ...emptyForm },
attendancePoints: this.checkedPointMap([]),
formVisible: false,
})
await this.load(1)
} catch (error) {
wx.showToast({ title: error.message, icon: 'none' })
} finally {
wx.hideLoading()
}
},
remove(e) {
const index = e.currentTarget.dataset.index
const notice = this.data.rows[index]
wx.showModal({
title: '删除通知',
content: notice.title,
success: async res => {
if (!res.confirm) {
return
}
try {
await api.deleteNotice(notice.id)
await this.load(1)
} catch (error) {
wx.showToast({ title: error.message, icon: 'none' })
}
},
})
},
prevPage() {
if (this.data.page > 1) {
this.load(this.data.page - 1)
}
},
nextPage() {
if (this.data.page < this.data.totalPages) {
this.load(this.data.page + 1)
}
},
onPageInput(e) {
this.setData({ pageInput: e.detail.value })
},
jumpToPage(e) {
wx.hideKeyboard()
const inputValue = e && e.detail && e.detail.value !== undefined ? e.detail.value : this.data.pageInput
const target = Math.max(1, Math.min(Number(inputValue) || this.data.page, this.data.totalPages))
if (target === this.data.page) {
this.setData({ pageInput: String(this.data.page || 1) })
return
}
this.load(target)
},
})
+3
View File
@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "通知配置"
}
+91
View File
@@ -0,0 +1,91 @@
<scroll-view
class="page safe-bottom"
scroll-y
type="list"
refresher-enabled
refresher-triggered="{{refreshing}}"
bindrefresherrefresh="onPullDownRefresh"
>
<view class="header">
<text class="title">通知配置</text>
<text class="subtitle">首页通知内容维护</text>
</view>
<view class="card action-panel">
<button class="btn primary full add-btn" bindtap="openCreate">新增通知</button>
</view>
<view wx:if="{{!rows.length}}" class="list-empty">暂无通知</view>
<view wx:for="{{rows}}" wx:key="id" wx:for-index="index" class="card notice-row">
<view class="row">
<view class="row-left">
<text class="value">{{item.title}}</text>
<text class="subtitle">考勤点 {{item.attendancePointsText || '-'}}</text>
<text class="subtitle">排序 {{item.sortOrder}} · {{item.updatedAtText}}</text>
</view>
<text class="pill {{item.isActive ? 'success' : 'warn'}}">{{item.isActive ? '展示中' : '已停用'}}</text>
</view>
<text class="notice-content">{{item.content}}</text>
<view class="button-row">
<button class="btn secondary full" data-index="{{index}}" bindtap="edit">编辑</button>
<button class="btn danger full" data-index="{{index}}" bindtap="remove">删除</button>
</view>
</view>
<view class="button-row pager-row" wx:if="{{rows.length}}">
<button class="btn secondary pager-nav" bindtap="prevPage" disabled="{{page <= 1}}">上一页</button>
<view class="page-jump-control">
<input class="page-jump-input" type="number" confirm-type="done" value="{{pageInput}}" bindinput="onPageInput" bindconfirm="jumpToPage" />
<text class="page-total-label">/ {{totalPages}}</text>
<text class="page-jump-action" bindtap="jumpToPage">跳</text>
</view>
<button class="btn secondary pager-nav" bindtap="nextPage" disabled="{{page >= totalPages}}">下一页</button>
</view>
</scroll-view>
<view wx:if="{{formVisible}}" class="modal-mask" catchtap="closeForm">
<view class="modal-panel" catchtap="noop">
<view class="modal-head">
<text class="modal-title">{{formTitle}}</text>
<button class="btn mini secondary modal-close" bindtap="clearForm">清空</button>
</view>
<view class="form-item">
<text class="label">标题</text>
<input class="input" value="{{form.title}}" data-field="title" bindinput="onInput" />
</view>
<view class="form-item">
<text class="label">内容</text>
<textarea class="textarea notice-textarea" value="{{form.content}}" data-field="content" bindinput="onInput"></textarea>
</view>
<view class="form-item">
<text class="label">通知考勤点</text>
<checkbox-group class="checkbox-grid" bindchange="onPointCheckboxChange">
<label
wx:for="{{attendancePoints}}"
wx:key="name"
class="checkbox-chip {{item.checked ? 'checked' : ''}}"
>
<checkbox value="{{item.name}}" checked="{{item.checked}}" color="#1463ff"></checkbox>
<text>{{item.name}}</text>
</label>
</checkbox-group>
</view>
<view class="form-grid">
<view class="form-item">
<text class="label">排序</text>
<input class="input number-input" type="number" value="{{form.sortOrder}}" data-field="sortOrder" bindinput="onInput" />
</view>
<view class="form-item status-form-item">
<text class="label">展示状态</text>
<view class="switch-row">
<text class="value">{{form.isActive ? '展示中' : '停用'}}</text>
<switch checked="{{form.isActive}}" bindchange="onActiveChange" color="#1463ff"></switch>
</view>
</view>
</view>
<view class="button-row">
<button class="btn secondary full" bindtap="closeForm">关闭</button>
<button class="btn primary full" bindtap="save">保存通知</button>
</view>
</view>
</view>
+131
View File
@@ -0,0 +1,131 @@
.action-panel {
padding: 22rpx;
}
.notice-row {
border-left: 6rpx solid #1463ff;
}
.notice-content {
display: -webkit-box;
overflow: hidden;
margin-top: 18rpx;
color: #344054;
font-size: 26rpx;
line-height: 1.55;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18rpx;
}
.notice-textarea {
min-height: 180rpx;
}
.switch-row {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 78rpx;
padding: 0 6rpx 0 20rpx;
border: 1rpx solid #ccd8e6;
border-radius: 14rpx;
background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%);
box-shadow:
inset 0 4rpx 8rpx rgba(22, 34, 50, 0.05),
0 6rpx 0 rgba(205, 216, 229, 0.48),
0 10rpx 18rpx rgba(22, 34, 50, 0.06);
box-sizing: border-box;
}
.status-form-item {
margin-bottom: 0;
}
.add-btn {
margin-top: 0;
}
.modal-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
background: rgba(15, 23, 42, 0.48);
box-sizing: border-box;
}
.modal-panel {
width: 100%;
max-height: 82vh;
overflow-y: auto;
padding: 30rpx;
border: 1rpx solid #e4e9f1;
border-radius: 18rpx;
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
box-shadow:
0 16rpx 0 rgba(170, 184, 204, 0.62),
0 30rpx 64rpx rgba(15, 23, 42, 0.24),
inset 0 2rpx 0 rgba(255, 255, 255, 0.96);
box-sizing: border-box;
}
.modal-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18rpx;
margin-bottom: 24rpx;
}
.modal-title {
color: #162232;
font-size: 34rpx;
font-weight: 700;
}
.modal-close {
flex: 0 0 auto;
margin: 0;
}
.checkbox-grid {
display: flex;
flex-direction: column;
gap: 14rpx;
}
.checkbox-chip {
display: flex;
align-items: center;
gap: 12rpx;
min-height: 72rpx;
padding: 0 18rpx;
border: 2rpx solid #cbd6e6;
border-radius: 14rpx;
background: linear-gradient(180deg, #ffffff 0%, #f5f8fc 100%);
color: #162232;
font-size: 25rpx;
font-weight: 700;
box-shadow:
0 6rpx 0 rgba(166, 181, 203, 0.42),
inset 0 2rpx 0 rgba(255, 255, 255, 0.92);
box-sizing: border-box;
}
.checkbox-chip.checked {
border-color: #7aa7f5;
background: linear-gradient(180deg, #eef5ff 0%, #ddeaff 100%);
color: #1456c2;
}