优化日程界面
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"pid":4360,"startedAt":1784109827988}
|
||||
+2
-2
@@ -20,8 +20,8 @@
|
||||
page {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background-color: #f8fafc;
|
||||
color: #1e293b;
|
||||
background-color: #f5f7fa;
|
||||
color: #303133;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { generateId } from '@/api/request.js'
|
||||
|
||||
const STORAGE_KEY = 'app_notes'
|
||||
|
||||
function readLocal() {
|
||||
return uni.getStorageSync(STORAGE_KEY) || []
|
||||
}
|
||||
|
||||
function writeLocal(list) {
|
||||
uni.setStorageSync(STORAGE_KEY, list)
|
||||
}
|
||||
|
||||
function seedIfEmpty() {
|
||||
const list = readLocal()
|
||||
if (list.length) return list
|
||||
const now = Date.now()
|
||||
const seeded = [
|
||||
{
|
||||
id: generateId(),
|
||||
title: '欢迎使用记事本',
|
||||
content: '在这里记录灵感、待办或任何想法。支持置顶、搜索与编辑,后续可一键接入云端同步。',
|
||||
pinned: true,
|
||||
createdAt: now - 86400000,
|
||||
updatedAt: now - 3600000
|
||||
},
|
||||
{
|
||||
id: generateId(),
|
||||
title: '项目会议要点',
|
||||
content: '1. 确认 Q3 目标\n2. 排期评审\n3. 接口联调时间',
|
||||
pinned: false,
|
||||
createdAt: now - 172800000,
|
||||
updatedAt: now - 7200000
|
||||
}
|
||||
]
|
||||
writeLocal(seeded)
|
||||
return seeded
|
||||
}
|
||||
|
||||
function sortNotes(list) {
|
||||
return [...list].sort((a, b) => {
|
||||
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1
|
||||
return (b.updatedAt || 0) - (a.updatedAt || 0)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取笔记列表
|
||||
* @param {{ keyword?: string }} params
|
||||
* @returns {Promise<{ list: Array, total: number }>}
|
||||
*/
|
||||
export async function fetchNotes(params = {}) {
|
||||
// TODO: return request({ url: '/api/notes', data: params })
|
||||
let list = seedIfEmpty()
|
||||
const keyword = (params.keyword || '').trim().toLowerCase()
|
||||
if (keyword) {
|
||||
list = list.filter(
|
||||
item =>
|
||||
item.title.toLowerCase().includes(keyword) ||
|
||||
item.content.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
list = sortNotes(list)
|
||||
return { list, total: list.length }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条笔记
|
||||
* @param {string} id
|
||||
*/
|
||||
export async function getNote(id) {
|
||||
// TODO: return request({ url: `/api/notes/${id}` })
|
||||
const list = readLocal()
|
||||
return list.find(item => item.id === id) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建笔记
|
||||
* @param {{ title: string, content: string, pinned?: boolean }} data
|
||||
*/
|
||||
export async function createNote(data) {
|
||||
// TODO: return request({ url: '/api/notes', method: 'POST', data })
|
||||
const now = Date.now()
|
||||
const note = {
|
||||
id: generateId(),
|
||||
title: (data.title || '').trim() || '无标题',
|
||||
content: (data.content || '').trim(),
|
||||
pinned: !!data.pinned,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
const list = readLocal()
|
||||
list.unshift(note)
|
||||
writeLocal(list)
|
||||
return note
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新笔记
|
||||
* @param {string} id
|
||||
* @param {{ title?: string, content?: string, pinned?: boolean }} data
|
||||
*/
|
||||
export async function updateNote(id, data) {
|
||||
// TODO: return request({ url: `/api/notes/${id}`, method: 'PUT', data })
|
||||
const list = readLocal()
|
||||
const index = list.findIndex(item => item.id === id)
|
||||
if (index === -1) throw new Error('笔记不存在')
|
||||
const note = {
|
||||
...list[index],
|
||||
...data,
|
||||
title: data.title !== undefined ? (data.title.trim() || '无标题') : list[index].title,
|
||||
content: data.content !== undefined ? data.content.trim() : list[index].content,
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
list[index] = note
|
||||
writeLocal(list)
|
||||
return note
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除笔记
|
||||
* @param {string} id
|
||||
*/
|
||||
export async function deleteNote(id) {
|
||||
// TODO: return request({ url: `/api/notes/${id}`, method: 'DELETE' })
|
||||
const list = readLocal().filter(item => item.id !== id)
|
||||
writeLocal(list)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换置顶
|
||||
* @param {string} id
|
||||
*/
|
||||
export async function toggleNotePin(id) {
|
||||
const note = await getNote(id)
|
||||
if (!note) throw new Error('笔记不存在')
|
||||
return updateNote(id, { pinned: !note.pinned })
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 统一请求封装,接入后端时在此配置 baseURL、token、错误处理。
|
||||
*
|
||||
* 示例:
|
||||
* export function request({ url, method = 'GET', data }) {
|
||||
* return new Promise((resolve, reject) => {
|
||||
* uni.request({
|
||||
* url: BASE_URL + url,
|
||||
* method,
|
||||
* data,
|
||||
* header: { Authorization: 'Bearer ' + getToken() },
|
||||
* success: (res) => {
|
||||
* if (res.statusCode >= 200 && res.statusCode < 300) resolve(res.data)
|
||||
* else reject(res)
|
||||
* },
|
||||
* fail: reject
|
||||
* })
|
||||
* })
|
||||
* }
|
||||
*/
|
||||
|
||||
export function generateId() {
|
||||
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import { generateId } from '@/api/request.js'
|
||||
import { startOfDay, endOfDay } from '@/utils/date.js'
|
||||
|
||||
const STORAGE_KEY = 'app_schedules'
|
||||
|
||||
const PRIORITY = {
|
||||
low: { value: 'low', label: '低', color: '#909399' },
|
||||
medium: { value: 'medium', label: '中', color: '#3c9cff' },
|
||||
high: { value: 'high', label: '高', color: '#f56c6c' }
|
||||
}
|
||||
|
||||
export const PRIORITY_OPTIONS = Object.values(PRIORITY)
|
||||
|
||||
export const REMIND_CHANNEL_OPTIONS = [
|
||||
{ value: 'sms', label: '短信' },
|
||||
{ value: 'email', label: '邮件' },
|
||||
{ value: 'bark', label: 'Bark推送' },
|
||||
{ value: 'site', label: '站内信' },
|
||||
{ value: 'app', label: 'APP' }
|
||||
]
|
||||
|
||||
export const REMIND_MINUTES_OPTIONS = [
|
||||
{ label: '不提前', value: 0 },
|
||||
{ label: '提前 5 分钟', value: 5 },
|
||||
{ label: '提前 10 分钟', value: 10 },
|
||||
{ label: '提前 15 分钟', value: 15 },
|
||||
{ label: '提前 30 分钟', value: 30 },
|
||||
{ label: '提前 1 小时', value: 60 },
|
||||
{ label: '提前 2 小时', value: 120 },
|
||||
{ label: '提前 1 天', value: 1440 }
|
||||
]
|
||||
|
||||
function readLocal() {
|
||||
const list = uni.getStorageSync(STORAGE_KEY) || []
|
||||
return list.map(normalizeSchedule)
|
||||
}
|
||||
|
||||
function writeLocal(list) {
|
||||
uni.setStorageSync(STORAGE_KEY, list)
|
||||
}
|
||||
|
||||
export function normalizeSchedule(item) {
|
||||
const content = (item.content || item.title || '').trim()
|
||||
const title = (item.title || content.split('\n')[0] || '无标题').trim().slice(0, 60)
|
||||
let remindChannels = item.remindChannels
|
||||
if (!Array.isArray(remindChannels)) {
|
||||
remindChannels = item.remindMinutes > 0 ? ['app'] : []
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
title,
|
||||
content,
|
||||
remindChannels,
|
||||
remindMinutes: item.remindMinutes ?? 15
|
||||
}
|
||||
}
|
||||
|
||||
function buildTitle(content) {
|
||||
const line = (content || '').split('\n')[0].trim()
|
||||
return line.slice(0, 60) || '无标题'
|
||||
}
|
||||
|
||||
function seedIfEmpty() {
|
||||
const raw = uni.getStorageSync(STORAGE_KEY)
|
||||
if (raw && raw.length) return raw.map(normalizeSchedule)
|
||||
const now = Date.now()
|
||||
const today = new Date()
|
||||
const seeded = [
|
||||
{
|
||||
id: generateId(),
|
||||
content: '团队周会\n汇报本周进度,讨论下周计划',
|
||||
datetime: new Date(today.getFullYear(), today.getMonth(), today.getDate(), 10, 0).getTime(),
|
||||
remindChannels: ['app', 'site'],
|
||||
remindMinutes: 15,
|
||||
priority: 'high',
|
||||
completed: false,
|
||||
createdAt: now - 86400000,
|
||||
updatedAt: now - 86400000
|
||||
},
|
||||
{
|
||||
id: generateId(),
|
||||
content: '提交月报\n整理本月数据并发送给主管',
|
||||
datetime: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1, 18, 0).getTime(),
|
||||
remindChannels: ['email', 'site'],
|
||||
remindMinutes: 30,
|
||||
priority: 'medium',
|
||||
completed: false,
|
||||
createdAt: now - 43200000,
|
||||
updatedAt: now - 43200000
|
||||
},
|
||||
{
|
||||
id: generateId(),
|
||||
content: '健身打卡\n有氧运动 30 分钟',
|
||||
datetime: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1, 19, 30).getTime(),
|
||||
remindChannels: ['bark', 'app'],
|
||||
remindMinutes: 10,
|
||||
priority: 'low',
|
||||
completed: true,
|
||||
createdAt: now - 259200000,
|
||||
updatedAt: now - 86400000
|
||||
}
|
||||
].map(normalizeSchedule)
|
||||
writeLocal(seeded)
|
||||
return seeded
|
||||
}
|
||||
|
||||
function sortSchedules(list) {
|
||||
return [...list].sort((a, b) => {
|
||||
if (a.completed !== b.completed) return a.completed ? 1 : -1
|
||||
return (a.datetime || 0) - (b.datetime || 0)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日程列表
|
||||
*/
|
||||
export async function fetchSchedules(params = {}) {
|
||||
// TODO: return request({ url: '/api/schedules', data: params })
|
||||
let list = seedIfEmpty()
|
||||
const keyword = (params.keyword || '').trim().toLowerCase()
|
||||
const status = params.status || 'all'
|
||||
|
||||
if (keyword) {
|
||||
list = list.filter(item => {
|
||||
const text = `${item.title} ${item.content}`.toLowerCase()
|
||||
return text.includes(keyword)
|
||||
})
|
||||
}
|
||||
if (status === 'pending') list = list.filter(item => !item.completed)
|
||||
if (status === 'done') list = list.filter(item => item.completed)
|
||||
|
||||
if (params.date) {
|
||||
const dayStart = startOfDay(params.date).getTime()
|
||||
const dayEnd = endOfDay(params.date).getTime()
|
||||
list = list.filter(item => item.datetime >= dayStart && item.datetime <= dayEnd)
|
||||
}
|
||||
|
||||
list = sortSchedules(list)
|
||||
return { list, total: list.length }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条日程
|
||||
*/
|
||||
export async function getSchedule(id) {
|
||||
// TODO: return request({ url: `/api/schedules/${id}` })
|
||||
const list = readLocal()
|
||||
const item = list.find(entry => entry.id === id)
|
||||
return item ? normalizeSchedule(item) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建日程
|
||||
* @param {{ content: string, datetime: number, remindChannels?: string[], remindMinutes?: number }} data
|
||||
*/
|
||||
export async function createSchedule(data) {
|
||||
// TODO: return request({ url: '/api/schedules', method: 'POST', data })
|
||||
const now = Date.now()
|
||||
const content = (data.content || '').trim()
|
||||
const schedule = normalizeSchedule({
|
||||
id: generateId(),
|
||||
title: buildTitle(content),
|
||||
content,
|
||||
datetime: data.datetime,
|
||||
remindChannels: data.remindChannels || [],
|
||||
remindMinutes: data.remindMinutes ?? 15,
|
||||
priority: data.priority || 'medium',
|
||||
completed: false,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
})
|
||||
const list = readLocal()
|
||||
list.push(schedule)
|
||||
writeLocal(list)
|
||||
return schedule
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新日程
|
||||
*/
|
||||
export async function updateSchedule(id, data) {
|
||||
// TODO: return request({ url: `/api/schedules/${id}`, method: 'PUT', data })
|
||||
const list = readLocal()
|
||||
const index = list.findIndex(item => item.id === id)
|
||||
if (index === -1) throw new Error('日程不存在')
|
||||
|
||||
const prev = list[index]
|
||||
const content = data.content !== undefined ? data.content.trim() : prev.content
|
||||
const schedule = normalizeSchedule({
|
||||
...prev,
|
||||
...data,
|
||||
title: data.content !== undefined ? buildTitle(content) : prev.title,
|
||||
content,
|
||||
updatedAt: Date.now()
|
||||
})
|
||||
list[index] = schedule
|
||||
writeLocal(list)
|
||||
return schedule
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除日程
|
||||
*/
|
||||
export async function deleteSchedule(id) {
|
||||
// TODO: return request({ url: `/api/schedules/${id}`, method: 'DELETE' })
|
||||
const list = readLocal().filter(item => item.id !== id)
|
||||
writeLocal(list)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换完成状态
|
||||
*/
|
||||
export async function toggleScheduleComplete(id) {
|
||||
const item = await getSchedule(id)
|
||||
if (!item) throw new Error('日程不存在')
|
||||
return updateSchedule(id, { completed: !item.completed })
|
||||
}
|
||||
|
||||
export function getPriorityMeta(value) {
|
||||
return PRIORITY[value] || PRIORITY.medium
|
||||
}
|
||||
|
||||
export function getRemindChannelLabel(value) {
|
||||
return REMIND_CHANNEL_OPTIONS.find(item => item.value === value)?.label || value
|
||||
}
|
||||
|
||||
export function getRemindChannelLabels(channels = []) {
|
||||
if (!channels.length) return '未设置'
|
||||
return channels.map(getRemindChannelLabel).join('、')
|
||||
}
|
||||
|
||||
export function getRemindMinutesLabel(minutes) {
|
||||
const item = REMIND_MINUTES_OPTIONS.find(option => option.value === minutes)
|
||||
return item?.label || (minutes ? `提前 ${minutes} 分钟` : '不提前')
|
||||
}
|
||||
|
||||
export function getScheduleSummary(item) {
|
||||
const text = (item?.content || item?.title || '').trim()
|
||||
const line = text.split('\n')[0]
|
||||
return line || '无内容'
|
||||
}
|
||||
+13
-105
@@ -1,179 +1,87 @@
|
||||
<template>
|
||||
|
||||
<view class="app-tabbar">
|
||||
|
||||
<view class="tabbar-pill">
|
||||
|
||||
<view class="tabbar-inner">
|
||||
<view
|
||||
|
||||
v-for="tab in tabs"
|
||||
|
||||
:key="tab.name"
|
||||
|
||||
class="tab-item"
|
||||
|
||||
:class="{ active: current === tab.name }"
|
||||
|
||||
@tap="onChange(tab.name)"
|
||||
|
||||
>
|
||||
|
||||
<FaIcon
|
||||
|
||||
:name="tab.icon"
|
||||
|
||||
:color="current === tab.name ? '#3c9cff' : '#9acafc'"
|
||||
|
||||
:size="20"
|
||||
|
||||
:color="current === tab.name ? '#3c9cff' : '#909399'"
|
||||
:size="22"
|
||||
/>
|
||||
|
||||
<text class="tab-text">{{ tab.text }}</text>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<view class="safe-area" />
|
||||
|
||||
</view>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
current: {
|
||||
|
||||
type: String,
|
||||
|
||||
default: 'dashboard'
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
const tabs = [
|
||||
|
||||
{ name: 'dashboard', text: '仪表盘', icon: 'table-cells' },
|
||||
|
||||
{ name: 'features', text: '功能', icon: 'grip' },
|
||||
|
||||
{ name: 'profile', text: '我的', icon: 'user' }
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
||||
const routes = {
|
||||
|
||||
dashboard: '/pages/dashboard/dashboard',
|
||||
|
||||
features: '/pages/features/features',
|
||||
|
||||
profile: '/pages/profile/profile'
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function onChange(name) {
|
||||
|
||||
if (name === props.current) return
|
||||
|
||||
uni.redirectTo({ url: routes[name] })
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.app-tabbar {
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
padding: 16rpx 32rpx 0;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border-top: 1rpx solid #ebeef5;
|
||||
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tabbar-pill {
|
||||
|
||||
.tabbar-inner {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-around;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border: 2rpx solid #e8f0f8;
|
||||
|
||||
border-radius: 999rpx;
|
||||
|
||||
padding: 16rpx 12rpx;
|
||||
|
||||
padding: 12rpx 0 8rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tab-item {
|
||||
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 6rpx;
|
||||
|
||||
gap: 4rpx;
|
||||
padding: 8rpx 0;
|
||||
|
||||
&.active .tab-text {
|
||||
color: #3c9cff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tab-text {
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
color: #9acafc;
|
||||
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
|
||||
color: #3c9cff;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.safe-area {
|
||||
|
||||
height: constant(safe-area-inset-bottom);
|
||||
|
||||
height: env(safe-area-inset-bottom);
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,41 @@
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tools/notepad/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "记事本",
|
||||
"navigationBarBackgroundColor": "#F8FAFC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tools/notepad/edit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "新建笔记",
|
||||
"navigationBarBackgroundColor": "#F8FAFC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tools/schedule/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "日程提醒",
|
||||
"navigationBarBackgroundColor": "#F8FAFC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tools/schedule/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "日程详情",
|
||||
"navigationBarBackgroundColor": "#F8FAFC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/tools/schedule/edit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "新建日程",
|
||||
"navigationBarBackgroundColor": "#F8FAFC"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="search-bar">
|
||||
<FaIcon name="magnifying-glass" color="#9acafc" :size="18" />
|
||||
<FaIcon name="magnifying-glass" color="#909399" :size="16" />
|
||||
<text class="search-text">搜索功能、数据...</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -19,17 +19,20 @@
|
||||
<view class="main-scroll">
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card" v-for="item in stats" :key="item.label">
|
||||
<text class="stat-value">{{ item.value }}</text>
|
||||
<text class="stat-label">{{ item.label }}</text>
|
||||
<text class="stat-trend" :class="item.up ? 'up' : 'down'">
|
||||
{{ item.up ? '↑' : '↓' }} {{ item.trend }}
|
||||
</text>
|
||||
<text class="stat-value">{{ item.value }}</text>
|
||||
<view class="stat-trend" :class="item.up ? 'up' : 'down'">
|
||||
<FaIcon :name="item.up ? 'arrow-trend-up' : 'arrow-trend-down'" :color="item.up ? '#3c9cff' : '#909399'" :size="12" />
|
||||
<text>{{ item.trend }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">快捷入口</text>
|
||||
<view class="quick-grid">
|
||||
</view>
|
||||
<view class="quick-card">
|
||||
<view
|
||||
class="quick-item"
|
||||
v-for="item in quickActions"
|
||||
@@ -37,7 +40,7 @@
|
||||
@tap="onQuickTap(item)"
|
||||
>
|
||||
<view class="quick-icon">
|
||||
<FaIcon :name="item.icon" color="#3c9cff" :size="22" />
|
||||
<FaIcon :name="item.icon" color="#3c9cff" :size="20" />
|
||||
</view>
|
||||
<text class="quick-name">{{ item.name }}</text>
|
||||
</view>
|
||||
@@ -51,11 +54,14 @@
|
||||
</view>
|
||||
<view class="activity-card">
|
||||
<view class="activity-item" v-for="(item, index) in activities" :key="index">
|
||||
<view class="activity-dot" />
|
||||
<view class="activity-icon">
|
||||
<FaIcon name="circle" color="#3c9cff" :size="8" />
|
||||
</view>
|
||||
<view class="activity-body">
|
||||
<text class="activity-title">{{ item.title }}</text>
|
||||
<text class="activity-time">{{ item.time }}</text>
|
||||
</view>
|
||||
<FaIcon name="chevron-right" color="#c0c4cc" :size="12" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -121,149 +127,145 @@ function onQuickTap(item) {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.page {
|
||||
height: 100vh;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@include page-shell;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
padding: calc(var(--status-bar-height, 44px) + 24rpx) 40rpx 32rpx;
|
||||
background: #ffffff;
|
||||
padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 24rpx;
|
||||
background: $color-card;
|
||||
box-shadow: $shadow-header;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: $u-primary-disabled;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.username {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: $u-primary;
|
||||
margin-top: 6rpx;
|
||||
color: $color-text;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #e8f0f8;
|
||||
background: $color-primary-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 32rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: $u-primary;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #e8f0f8;
|
||||
border-radius: 16rpx;
|
||||
padding: 22rpx 28rpx;
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.search-text {
|
||||
font-size: 26rpx;
|
||||
color: $u-primary-disabled;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.main-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 8rpx 40rpx 0;
|
||||
@include scroll-body;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 48rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #e8f0f8;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: $u-primary;
|
||||
@include card;
|
||||
padding: 28rpx 24rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: $u-primary-disabled;
|
||||
margin-top: 6rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
margin-top: 16rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: $radius-sm;
|
||||
font-size: 22rpx;
|
||||
|
||||
&.up {
|
||||
color: $u-primary;
|
||||
background: $color-primary-bg;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: $u-primary-disabled;
|
||||
background: $color-divider;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 48rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: $u-primary;
|
||||
@include section-title;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-size: 24rpx;
|
||||
color: $u-primary-disabled;
|
||||
font-size: 26rpx;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
.quick-card {
|
||||
@include card;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
padding: 32rpx 16rpx;
|
||||
}
|
||||
|
||||
.quick-item {
|
||||
@@ -271,72 +273,67 @@ function onQuickTap(item) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #e8f0f8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active {
|
||||
border-color: $u-primary-disabled;
|
||||
}
|
||||
@include icon-box(88rpx);
|
||||
border-radius: $radius-lg;
|
||||
}
|
||||
|
||||
.quick-name {
|
||||
font-size: 22rpx;
|
||||
color: $u-primary-disabled;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #e8f0f8;
|
||||
border-radius: 16rpx;
|
||||
@include card;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx;
|
||||
gap: 20rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
gap: 16rpx;
|
||||
|
||||
& + & {
|
||||
border-top: 2rpx solid #f0f6fb;
|
||||
border-top: 1rpx solid $color-divider;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $color-bg-page;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: $u-primary;
|
||||
flex-shrink: 0;
|
||||
.activity-icon {
|
||||
width: 32rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.activity-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: $u-primary;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: $u-primary-disabled;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.bottom-space {
|
||||
height: 40rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
+234
-381
@@ -1,451 +1,304 @@
|
||||
<template>
|
||||
|
||||
<view class="page">
|
||||
|
||||
<view class="page-header">
|
||||
|
||||
<text class="page-title">功能中心</text>
|
||||
|
||||
<text class="page-desc">探索更多精彩功能</text>
|
||||
|
||||
<view class="search-wrap">
|
||||
<view class="search-bar">
|
||||
<FaIcon name="magnifying-glass" color="#909399" :size="16" />
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
placeholder="搜索功能名称"
|
||||
placeholder-class="search-placeholder"
|
||||
confirm-type="search"
|
||||
/>
|
||||
<view v-if="keyword" class="search-clear" @tap="keyword = ''">
|
||||
<FaIcon name="circle-xmark" color="#c0c4cc" :size="16" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view class="main-scroll">
|
||||
|
||||
<view class="feature-banner" @tap="onBannerTap">
|
||||
|
||||
<view class="banner-content">
|
||||
|
||||
<text class="banner-title">新功能上线</text>
|
||||
|
||||
<text class="banner-desc">AI 智能助手,效率翻倍</text>
|
||||
|
||||
</view>
|
||||
|
||||
<view class="banner-btn">立即体验</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view class="category" v-for="cat in categories" :key="cat.title">
|
||||
|
||||
<text class="category-title">{{ cat.title }}</text>
|
||||
|
||||
<view class="feature-grid">
|
||||
|
||||
<view class="body">
|
||||
<view class="sidebar">
|
||||
<view
|
||||
|
||||
class="feature-card"
|
||||
|
||||
v-for="item in cat.items"
|
||||
|
||||
:key="item.name"
|
||||
|
||||
@tap="onFeatureTap(item)"
|
||||
|
||||
v-for="(cat, index) in categories"
|
||||
:key="cat.id"
|
||||
class="category-item"
|
||||
:class="{ active: activeIndex === index }"
|
||||
@tap="selectCategory(index)"
|
||||
>
|
||||
|
||||
<view class="feature-icon">
|
||||
|
||||
<FaIcon :name="item.icon" color="#3c9cff" :size="22" />
|
||||
|
||||
<text class="category-text">{{ cat.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="feature-name">{{ item.name }}</text>
|
||||
|
||||
<text class="feature-desc">{{ item.desc }}</text>
|
||||
|
||||
<view class="content">
|
||||
<view v-if="displayItems.length === 0" class="empty">
|
||||
<FaIcon name="magnifying-glass" color="#c0c4cc" :size="32" />
|
||||
<text class="empty-text">未找到相关功能</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="item in displayItems"
|
||||
:key="item.id"
|
||||
class="item-card"
|
||||
:class="{ active: activeItemId === item.id }"
|
||||
@tap="onItemTap(item)"
|
||||
>
|
||||
<text class="item-name">{{ item.name }}</text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view class="bottom-space" />
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<AppTabbar current="features" />
|
||||
|
||||
</view>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { isLoggedIn } from '@/utils/auth.js'
|
||||
|
||||
import AppTabbar from '@/components/AppTabbar.vue'
|
||||
|
||||
const keyword = ref('')
|
||||
const activeIndex = ref(0)
|
||||
const activeItemId = ref('')
|
||||
|
||||
const routeMap = {
|
||||
notepad: '/pages/tools/notepad/index',
|
||||
schedule: '/pages/tools/schedule/index'
|
||||
}
|
||||
|
||||
const categories = ref([
|
||||
|
||||
{
|
||||
|
||||
id: 'handy',
|
||||
title: '便捷工具',
|
||||
items: [
|
||||
{ id: 'notepad', name: '记事本', route: routeMap.notepad },
|
||||
{ id: 'schedule', name: '日程提醒', route: routeMap.schedule }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'tools',
|
||||
title: '常用工具',
|
||||
|
||||
items: [
|
||||
|
||||
{ name: '扫一扫', desc: '快速识别', icon: 'qrcode' },
|
||||
|
||||
{ name: '收付款', desc: '便捷支付', icon: 'coins' },
|
||||
|
||||
{ name: '卡包', desc: '优惠券', icon: 'ticket' },
|
||||
|
||||
{ name: '出行', desc: '交通服务', icon: 'car' }
|
||||
|
||||
{ id: 't1', name: '扫一扫' },
|
||||
{ id: 't2', name: '收付款' },
|
||||
{ id: 't3', name: '卡包管理' },
|
||||
{ id: 't4', name: '出行服务' },
|
||||
{ id: 't5', name: '二维码生成' },
|
||||
{ id: 't6', name: '发票助手' }
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
id: 'life',
|
||||
title: '生活服务',
|
||||
|
||||
items: [
|
||||
|
||||
{ name: '外卖', desc: '美食到家', icon: 'bag-shopping' },
|
||||
|
||||
{ name: '电影', desc: '在线购票', icon: 'film' },
|
||||
|
||||
{ name: '酒店', desc: '预订住宿', icon: 'hotel' },
|
||||
|
||||
{ name: '更多', desc: '发现更多', icon: 'ellipsis' }
|
||||
|
||||
{ id: 'l1', name: '外卖订餐' },
|
||||
{ id: 'l2', name: '电影购票' },
|
||||
{ id: 'l3', name: '酒店预订' },
|
||||
{ id: 'l4', name: '快递查询' },
|
||||
{ id: 'l5', name: '家政预约' },
|
||||
{ id: 'l6', name: '更多服务' }
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
id: 'office',
|
||||
title: '办公效率',
|
||||
|
||||
items: [
|
||||
|
||||
{ name: '文档', desc: '在线协作', icon: 'file-lines' },
|
||||
|
||||
{ name: '日历', desc: '日程管理', icon: 'calendar-days' },
|
||||
|
||||
{ name: '云盘', desc: '文件存储', icon: 'folder' },
|
||||
|
||||
{ name: '会议', desc: '视频会议', icon: 'video' }
|
||||
|
||||
{ id: 'o1', name: '文档协作' },
|
||||
{ id: 'o2', name: '日程管理' },
|
||||
{ id: 'o3', name: '云盘存储' },
|
||||
{ id: 'o4', name: '视频会议' },
|
||||
{ id: 'o5', name: '任务看板' },
|
||||
{ id: 'o6', name: '审批流程' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
title: '数据报表',
|
||||
items: [
|
||||
{ id: 'd1', name: '销售统计' },
|
||||
{ id: 'd2', name: '用户分析' },
|
||||
{ id: 'd3', name: '访问趋势' },
|
||||
{ id: 'd4', name: '转化报表' },
|
||||
{ id: 'd5', name: '导出数据' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'system',
|
||||
title: '系统设置',
|
||||
items: [
|
||||
{ id: 's1', name: '账号管理' },
|
||||
{ id: 's2', name: '权限配置' },
|
||||
{ id: 's3', name: '消息通知' },
|
||||
{ id: 's4', name: '安全中心' },
|
||||
{ id: 's5', name: '关于系统' }
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
])
|
||||
|
||||
const currentCategory = computed(() => categories.value[activeIndex.value])
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
|
||||
}
|
||||
|
||||
const displayItems = computed(() => {
|
||||
const items = currentCategory.value?.items || []
|
||||
const q = keyword.value.trim().toLowerCase()
|
||||
if (!q) return items
|
||||
return items.filter(item => item.name.toLowerCase().includes(q))
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function onBannerTap() {
|
||||
|
||||
uni.showToast({ title: 'AI 智能助手', icon: 'none' })
|
||||
|
||||
function selectCategory(index) {
|
||||
activeIndex.value = index
|
||||
activeItemId.value = ''
|
||||
}
|
||||
|
||||
|
||||
|
||||
function onFeatureTap(item) {
|
||||
|
||||
function onItemTap(item) {
|
||||
activeItemId.value = item.id
|
||||
if (item.route) {
|
||||
uni.navigateTo({ url: item.route })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: item.name, icon: 'none' })
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.page {
|
||||
|
||||
height: 100vh;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
@include page-shell;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.page-header {
|
||||
|
||||
.search-wrap {
|
||||
flex-shrink: 0;
|
||||
|
||||
padding: calc(var(--status-bar-height, 44px) + 24rpx) 40rpx 24rpx;
|
||||
|
||||
padding: calc(var(--status-bar-height, 44px) + 16rpx) 24rpx 20rpx;
|
||||
background: $color-card;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.page-title {
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: 40rpx;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: $u-primary;
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-full;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.page-desc {
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: 26rpx;
|
||||
|
||||
color: $u-primary-disabled;
|
||||
|
||||
margin-top: 6rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.main-scroll {
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
|
||||
min-height: 0;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
padding: 0 40rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.feature-banner {
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border: 2rpx solid #e8f0f8;
|
||||
|
||||
border-radius: 16rpx;
|
||||
|
||||
padding: 32rpx 28rpx;
|
||||
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
align-items: center;
|
||||
|
||||
margin-bottom: 48rpx;
|
||||
|
||||
|
||||
|
||||
&:active {
|
||||
|
||||
border-color: $u-primary-disabled;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.banner-title {
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: 30rpx;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: $u-primary;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.banner-desc {
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: 24rpx;
|
||||
|
||||
color: $u-primary-disabled;
|
||||
|
||||
margin-top: 8rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.banner-btn {
|
||||
|
||||
padding: 14rpx 28rpx;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border: 2rpx solid #e8f0f8;
|
||||
|
||||
border-radius: 16rpx;
|
||||
|
||||
font-size: 24rpx;
|
||||
|
||||
color: $u-primary;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.category {
|
||||
|
||||
margin-bottom: 48rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.category-title {
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: 30rpx;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: $u-primary;
|
||||
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.feature-grid {
|
||||
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
gap: 20rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.feature-card {
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border: 2rpx solid #e8f0f8;
|
||||
|
||||
border-radius: 16rpx;
|
||||
|
||||
padding: 28rpx;
|
||||
|
||||
|
||||
|
||||
&:active {
|
||||
|
||||
border-color: $u-primary-disabled;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.feature-icon {
|
||||
|
||||
width: 80rpx;
|
||||
|
||||
height: 80rpx;
|
||||
|
||||
border-radius: 20rpx;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border: 2rpx solid #e8f0f8;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.feature-name {
|
||||
|
||||
display: block;
|
||||
|
||||
height: 72rpx;
|
||||
font-size: 28rpx;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: $u-primary;
|
||||
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.feature-desc {
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
color: $u-primary-disabled;
|
||||
|
||||
margin-top: 4rpx;
|
||||
|
||||
.search-placeholder {
|
||||
color: $color-text-muted;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bottom-space {
|
||||
|
||||
height: 40rpx;
|
||||
|
||||
.search-clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: $color-card;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 200rpx;
|
||||
flex-shrink: 0;
|
||||
background: $color-bg-page;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
position: relative;
|
||||
padding: 32rpx 16rpx;
|
||||
text-align: center;
|
||||
|
||||
&.active {
|
||||
background: $color-card;
|
||||
|
||||
.category-text {
|
||||
color: $color-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6rpx;
|
||||
height: 36rpx;
|
||||
background: $color-primary;
|
||||
border-radius: 0 4rpx 4rpx 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.category-text {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-secondary;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 20rpx 24rpx;
|
||||
background: $color-card;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
padding: 28rpx 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
|
||||
&.active {
|
||||
background: $color-primary-bg;
|
||||
|
||||
.item-name {
|
||||
color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 28rpx;
|
||||
color: $color-text;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
+100
-78
@@ -3,6 +3,9 @@
|
||||
<view class="page-inner">
|
||||
<!-- 品牌区 -->
|
||||
<view class="header">
|
||||
<view class="logo">
|
||||
<text class="logo-char">云</text>
|
||||
</view>
|
||||
<text class="title">欢迎回来</text>
|
||||
<text class="subtitle">登录云泽,开启你的旅程</text>
|
||||
</view>
|
||||
@@ -22,6 +25,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 表单 -->
|
||||
<view class="form-card">
|
||||
<view class="form">
|
||||
<template v-if="loginType === 'phone'">
|
||||
<view class="field">
|
||||
@@ -31,7 +35,7 @@
|
||||
type="number"
|
||||
maxlength="11"
|
||||
border="none"
|
||||
color="#3c9cff"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
>
|
||||
@@ -47,7 +51,7 @@
|
||||
type="number"
|
||||
maxlength="6"
|
||||
border="none"
|
||||
color="#3c9cff"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
@@ -65,7 +69,7 @@
|
||||
v-model="username"
|
||||
placeholder="请输入账号"
|
||||
border="none"
|
||||
color="#3c9cff"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
@@ -76,7 +80,7 @@
|
||||
placeholder="请输入密码"
|
||||
type="password"
|
||||
border="none"
|
||||
color="#3c9cff"
|
||||
color="#303133"
|
||||
:customStyle="inputStyle"
|
||||
:placeholderStyle="placeholderStyle"
|
||||
/>
|
||||
@@ -86,6 +90,7 @@
|
||||
<view class="btn-primary" @tap="handleLogin">登 录</view>
|
||||
<view class="btn-ghost" @tap="handleTestLogin">测试登录</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 协议 -->
|
||||
<view class="agreement" @tap="agreed = !agreed">
|
||||
@@ -139,7 +144,7 @@ const inputStyle = {
|
||||
fontSize: '28rpx'
|
||||
}
|
||||
|
||||
const placeholderStyle = 'color: #9acafc; font-size: 28rpx'
|
||||
const placeholderStyle = 'color: #909399; font-size: 28rpx'
|
||||
|
||||
onMounted(() => {
|
||||
if (isLoggedIn()) {
|
||||
@@ -217,85 +222,85 @@ function socialLogin(type) {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: #ffffff;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.page-inner {
|
||||
padding: 0 56rpx;
|
||||
padding: 0 48rpx;
|
||||
padding-top: calc(var(--status-bar-height, 44px) + 80rpx);
|
||||
}
|
||||
|
||||
/* ---- 品牌 ---- */
|
||||
.header {
|
||||
margin-bottom: 72rpx;
|
||||
margin-bottom: 64rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 22rpx;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #e8f0f8;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: $radius-lg;
|
||||
background: $color-primary-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 36rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.logo-char {
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
color: $u-primary;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 52rpx;
|
||||
font-size: 48rpx;
|
||||
font-weight: 600;
|
||||
color: $u-primary;
|
||||
letter-spacing: 2rpx;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: $u-primary-disabled;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-muted;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* ---- Tab ---- */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 48rpx;
|
||||
margin-bottom: 48rpx;
|
||||
background: $color-card;
|
||||
border-radius: $radius-md;
|
||||
padding: 6rpx;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: $shadow-card;
|
||||
}
|
||||
|
||||
.tab {
|
||||
font-size: 30rpx;
|
||||
color: $u-primary-disabled;
|
||||
padding-bottom: 12rpx;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-secondary;
|
||||
padding: 18rpx 0;
|
||||
border-radius: $radius-sm;
|
||||
|
||||
&.active {
|
||||
color: $u-primary;
|
||||
color: $color-primary;
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 4rpx;
|
||||
background: $u-primary;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
background: $color-primary-bg;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 表单 ---- */
|
||||
.form-card {
|
||||
@include card;
|
||||
padding: 32rpx 28rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -303,10 +308,9 @@ function socialLogin(type) {
|
||||
}
|
||||
|
||||
.field {
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #e8f0f8;
|
||||
border-radius: 16rpx;
|
||||
padding: 0 28rpx;
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
padding: 0 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -317,71 +321,70 @@ function socialLogin(type) {
|
||||
|
||||
.prefix {
|
||||
font-size: 28rpx;
|
||||
color: $u-primary;
|
||||
color: $color-text;
|
||||
padding-right: 20rpx;
|
||||
margin-right: 20rpx;
|
||||
border-right: 1rpx solid $u-primary-disabled;
|
||||
border-right: 1rpx solid $color-border;
|
||||
}
|
||||
|
||||
.code-link {
|
||||
flex-shrink: 0;
|
||||
font-size: 26rpx;
|
||||
color: $u-primary;
|
||||
color: $color-primary;
|
||||
padding-left: 16rpx;
|
||||
white-space: nowrap;
|
||||
|
||||
&.disabled {
|
||||
color: $u-primary-disabled;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
height: 96rpx;
|
||||
background: $u-primary;
|
||||
border-radius: 16rpx;
|
||||
background: $color-primary;
|
||||
border-radius: $radius-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 6rpx;
|
||||
margin-top: 16rpx;
|
||||
margin-top: 8rpx;
|
||||
|
||||
&:active {
|
||||
background: $u-primary-dark;
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
height: 96rpx;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid $u-primary;
|
||||
border-radius: 16rpx;
|
||||
background: $color-card;
|
||||
border: 1rpx solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: $u-primary;
|
||||
color: $color-text-secondary;
|
||||
|
||||
&:active {
|
||||
background: #f8fbff;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 协议 ---- */
|
||||
.agreement {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12rpx;
|
||||
margin-top: 36rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.agree-dot {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid $u-primary-disabled;
|
||||
border: 1rpx solid $color-border;
|
||||
background: $color-card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -389,8 +392,8 @@ function socialLogin(type) {
|
||||
margin-top: 2rpx;
|
||||
|
||||
&.on {
|
||||
background: $u-primary;
|
||||
border-color: $u-primary;
|
||||
background: $color-primary;
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,18 +403,17 @@ function socialLogin(type) {
|
||||
}
|
||||
|
||||
.agree-text {
|
||||
font-size: 22rpx;
|
||||
color: $u-primary-disabled;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.agree-link {
|
||||
color: $u-primary;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
/* ---- 第三方 ---- */
|
||||
.oauth {
|
||||
margin-top: 80rpx;
|
||||
margin-top: 64rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -421,26 +423,46 @@ function socialLogin(type) {
|
||||
|
||||
.oauth-label {
|
||||
font-size: 24rpx;
|
||||
color: $u-primary-disabled;
|
||||
color: $color-text-muted;
|
||||
position: relative;
|
||||
padding: 0 32rpx;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 80rpx;
|
||||
height: 1rpx;
|
||||
background: $color-border;
|
||||
}
|
||||
|
||||
&::before {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.oauth-icons {
|
||||
display: flex;
|
||||
gap: 40rpx;
|
||||
gap: 48rpx;
|
||||
}
|
||||
|
||||
.oauth-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #e8f0f8;
|
||||
background: $color-card;
|
||||
box-shadow: $shadow-card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active {
|
||||
border-color: $u-primary-disabled;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,649 +1,306 @@
|
||||
<template>
|
||||
|
||||
<view class="page">
|
||||
|
||||
<view class="profile-header">
|
||||
|
||||
<view class="profile-info">
|
||||
|
||||
<view class="profile-avatar">
|
||||
|
||||
<text class="avatar-text">{{ avatarText }}</text>
|
||||
|
||||
</view>
|
||||
|
||||
<view class="profile-detail">
|
||||
|
||||
<text class="profile-name">{{ user?.nickname || '云泽用户' }}</text>
|
||||
|
||||
<text class="profile-id">ID: {{ userId }}</text>
|
||||
|
||||
</view>
|
||||
|
||||
<view class="profile-edit" @tap="onEdit">
|
||||
|
||||
<FaIcon name="pen-to-square" color="#3c9cff" :size="18" />
|
||||
|
||||
<FaIcon name="pen-to-square" color="#ffffff" :size="16" />
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view class="profile-stats">
|
||||
|
||||
<view class="profile-stat" v-for="item in profileStats" :key="item.label">
|
||||
|
||||
<text class="stat-num">{{ item.value }}</text>
|
||||
|
||||
<text class="stat-label">{{ item.label }}</text>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view class="main-scroll">
|
||||
|
||||
<view class="menu-group" v-for="(group, gIndex) in menuGroups" :key="gIndex">
|
||||
|
||||
<view
|
||||
|
||||
class="menu-item"
|
||||
|
||||
v-for="item in group"
|
||||
|
||||
:key="item.title"
|
||||
|
||||
@tap="onMenuTap(item)"
|
||||
|
||||
>
|
||||
|
||||
<view class="menu-left">
|
||||
|
||||
<view class="menu-icon">
|
||||
|
||||
<FaIcon :name="item.icon" color="#3c9cff" :size="18" />
|
||||
|
||||
</view>
|
||||
|
||||
<text class="menu-title">{{ item.title }}</text>
|
||||
|
||||
</view>
|
||||
|
||||
<view class="menu-right">
|
||||
|
||||
<text v-if="item.badge" class="menu-badge">{{ item.badge }}</text>
|
||||
|
||||
<FaIcon name="chevron-right" color="#9acafc" :size="14" />
|
||||
|
||||
<FaIcon name="chevron-right" color="#c0c4cc" :size="12" />
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view class="logout-btn" @tap="handleLogout">
|
||||
|
||||
<text class="logout-text">退出登录</text>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view class="bottom-space" />
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<AppTabbar current="profile" />
|
||||
|
||||
</view>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
import { getUser, logout, isLoggedIn } from '@/utils/auth.js'
|
||||
|
||||
import AppTabbar from '@/components/AppTabbar.vue'
|
||||
|
||||
|
||||
|
||||
const user = ref(null)
|
||||
|
||||
|
||||
|
||||
const avatarText = computed(() => {
|
||||
|
||||
const name = user.value?.nickname || '云'
|
||||
|
||||
return name.charAt(0).toUpperCase()
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
const userId = computed(() => {
|
||||
|
||||
return user.value?.phone
|
||||
|
||||
? user.value.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||
|
||||
: '10086'
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
const profileStats = ref([
|
||||
|
||||
{ label: '关注', value: '128' },
|
||||
|
||||
{ label: '粉丝', value: '256' },
|
||||
|
||||
{ label: '获赞', value: '1.2k' }
|
||||
|
||||
])
|
||||
|
||||
|
||||
|
||||
const menuGroups = ref([
|
||||
|
||||
[
|
||||
|
||||
{ title: '我的订单', icon: 'clipboard-list' },
|
||||
|
||||
{ title: '我的收藏', icon: 'star' },
|
||||
|
||||
{ title: '浏览历史', icon: 'clock-rotate-left' }
|
||||
|
||||
],
|
||||
|
||||
[
|
||||
|
||||
{ title: '消息通知', icon: 'bell', badge: '3' },
|
||||
|
||||
{ title: '账号安全', icon: 'lock' },
|
||||
|
||||
{ title: '隐私设置', icon: 'eye' }
|
||||
|
||||
],
|
||||
|
||||
[
|
||||
|
||||
{ title: '帮助中心', icon: 'circle-question' },
|
||||
|
||||
{ title: '关于我们', icon: 'circle-info' },
|
||||
|
||||
{ title: '设置', icon: 'gear' }
|
||||
|
||||
]
|
||||
|
||||
])
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
user.value = getUser()
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
function onEdit() {
|
||||
|
||||
uni.showToast({ title: '编辑资料', icon: 'none' })
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function onMenuTap(item) {
|
||||
|
||||
uni.showToast({ title: item.title, icon: 'none' })
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function handleLogout() {
|
||||
|
||||
uni.showModal({
|
||||
|
||||
title: '提示',
|
||||
|
||||
content: '确定要退出登录吗?',
|
||||
|
||||
success(res) {
|
||||
|
||||
if (res.confirm) {
|
||||
|
||||
logout()
|
||||
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.page {
|
||||
|
||||
height: 100vh;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
@include page-shell;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.profile-header {
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
padding: calc(var(--status-bar-height, 44px) + 24rpx) 40rpx 32rpx;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
padding: calc(var(--status-bar-height, 44px) + 20rpx) $page-padding-x 36rpx;
|
||||
background: $color-primary;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.profile-info {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 24rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.profile-avatar {
|
||||
|
||||
width: 100rpx;
|
||||
|
||||
height: 100rpx;
|
||||
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border: 2rpx solid #e8f0f8;
|
||||
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.35);
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.avatar-text {
|
||||
|
||||
font-size: 40rpx;
|
||||
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
|
||||
color: $u-primary;
|
||||
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.profile-detail {
|
||||
|
||||
flex: 1;
|
||||
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.profile-name {
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: 36rpx;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: $u-primary;
|
||||
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.profile-id {
|
||||
|
||||
display: block;
|
||||
|
||||
font-size: 24rpx;
|
||||
|
||||
color: $u-primary-disabled;
|
||||
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
margin-top: 6rpx;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.profile-edit {
|
||||
|
||||
width: 64rpx;
|
||||
|
||||
height: 64rpx;
|
||||
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border: 2rpx solid #e8f0f8;
|
||||
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
|
||||
|
||||
&:active {
|
||||
|
||||
border-color: $u-primary-disabled;
|
||||
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.profile-stats {
|
||||
|
||||
display: flex;
|
||||
|
||||
justify-content: space-around;
|
||||
|
||||
margin-top: 32rpx;
|
||||
|
||||
padding: 28rpx 0;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border: 2rpx solid #e8f0f8;
|
||||
|
||||
border-radius: 16rpx;
|
||||
|
||||
margin-top: 36rpx;
|
||||
padding-top: 28rpx;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.profile-stat {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.stat-num {
|
||||
|
||||
font-size: 32rpx;
|
||||
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
|
||||
color: $u-primary;
|
||||
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.stat-label {
|
||||
|
||||
font-size: 22rpx;
|
||||
|
||||
color: $u-primary-disabled;
|
||||
|
||||
margin-top: 4rpx;
|
||||
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.main-scroll {
|
||||
|
||||
flex: 1;
|
||||
|
||||
min-height: 0;
|
||||
|
||||
overflow-y: auto;
|
||||
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
padding: 8rpx 40rpx 0;
|
||||
|
||||
@include scroll-body;
|
||||
margin-top: -20rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.menu-group {
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border: 2rpx solid #e8f0f8;
|
||||
|
||||
border-radius: 16rpx;
|
||||
|
||||
@include card;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.menu-item {
|
||||
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
align-items: center;
|
||||
padding: 28rpx 24rpx;
|
||||
|
||||
padding: 28rpx;
|
||||
|
||||
border-bottom: 2rpx solid #f0f6fb;
|
||||
|
||||
|
||||
|
||||
&:last-child {
|
||||
|
||||
border-bottom: none;
|
||||
|
||||
& + & {
|
||||
border-top: 1rpx solid $color-divider;
|
||||
}
|
||||
|
||||
|
||||
|
||||
&:active {
|
||||
|
||||
background: #f8fafc;
|
||||
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.menu-left {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 20rpx;
|
||||
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.menu-icon {
|
||||
|
||||
width: 64rpx;
|
||||
|
||||
height: 64rpx;
|
||||
|
||||
border-radius: 16rpx;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border: 2rpx solid #e8f0f8;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
@include icon-box(64rpx);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.menu-title {
|
||||
|
||||
font-size: 28rpx;
|
||||
|
||||
color: $u-primary;
|
||||
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.menu-right {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 12rpx;
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.menu-badge {
|
||||
|
||||
font-size: 20rpx;
|
||||
|
||||
color: #ffffff;
|
||||
|
||||
background: $u-primary;
|
||||
|
||||
padding: 2rpx 12rpx;
|
||||
|
||||
border-radius: 999rpx;
|
||||
|
||||
background: $color-primary;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: $radius-full;
|
||||
min-width: 32rpx;
|
||||
|
||||
text-align: center;
|
||||
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.logout-btn {
|
||||
|
||||
margin-top: 8rpx;
|
||||
|
||||
@include card;
|
||||
height: 96rpx;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
border: 2rpx solid #e8f0f8;
|
||||
|
||||
border-radius: 16rpx;
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
|
||||
|
||||
&:active {
|
||||
|
||||
border-color: $u-primary-disabled;
|
||||
|
||||
background: #f8fafc;
|
||||
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.logout-text {
|
||||
|
||||
font-size: 30rpx;
|
||||
|
||||
color: $u-primary-disabled;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bottom-space {
|
||||
|
||||
height: 40rpx;
|
||||
|
||||
height: 24rpx;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="form-card">
|
||||
<view class="field">
|
||||
<text class="field-label">标题</text>
|
||||
<input
|
||||
class="field-input title-input"
|
||||
v-model="form.title"
|
||||
type="text"
|
||||
placeholder="输入标题"
|
||||
placeholder-class="placeholder"
|
||||
maxlength="80"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="field content-field">
|
||||
<text class="field-label">内容</text>
|
||||
<textarea
|
||||
class="field-textarea"
|
||||
v-model="form.content"
|
||||
placeholder="记录你的想法..."
|
||||
placeholder-class="placeholder"
|
||||
:auto-height="false"
|
||||
maxlength="5000"
|
||||
:show-confirm-bar="false"
|
||||
/>
|
||||
<text class="char-count">{{ form.content.length }}/5000</text>
|
||||
</view>
|
||||
|
||||
<view class="switch-row" @tap="form.pinned = !form.pinned">
|
||||
<view class="switch-left">
|
||||
<FaIcon name="thumbtack" :color="form.pinned ? '#3c9cff' : '#909399'" :size="16" />
|
||||
<text class="switch-label">置顶笔记</text>
|
||||
</view>
|
||||
<view class="switch-dot" :class="{ on: form.pinned }" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="noteId && meta.updatedAt" class="meta">
|
||||
<text>最近更新:{{ formatDate(meta.updatedAt, true) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<view v-if="noteId" class="btn-danger" @tap="onDelete">删除</view>
|
||||
<view class="btn-primary" :class="{ disabled: saving }" @tap="onSave">
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getNote, createNote, updateNote, deleteNote } from '@/api/note.js'
|
||||
import { formatDate } from '@/utils/date.js'
|
||||
|
||||
const noteId = ref('')
|
||||
const saving = ref(false)
|
||||
const form = reactive({
|
||||
title: '',
|
||||
content: '',
|
||||
pinned: false
|
||||
})
|
||||
const meta = reactive({
|
||||
updatedAt: 0
|
||||
})
|
||||
|
||||
onLoad(async (query) => {
|
||||
if (query?.id) {
|
||||
noteId.value = query.id
|
||||
uni.setNavigationBarTitle({ title: '编辑笔记' })
|
||||
await loadNote(query.id)
|
||||
} else {
|
||||
uni.setNavigationBarTitle({ title: '新建笔记' })
|
||||
}
|
||||
})
|
||||
|
||||
async function loadNote(id) {
|
||||
try {
|
||||
const note = await getNote(id)
|
||||
if (!note) {
|
||||
uni.showToast({ title: '笔记不存在', icon: 'none' })
|
||||
setTimeout(() => uni.navigateBack(), 800)
|
||||
return
|
||||
}
|
||||
form.title = note.title
|
||||
form.content = note.content
|
||||
form.pinned = !!note.pinned
|
||||
meta.updatedAt = note.updatedAt
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (saving.value) return
|
||||
if (!form.title.trim() && !form.content.trim()) {
|
||||
uni.showToast({ title: '请输入标题或内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
if (noteId.value) {
|
||||
await updateNote(noteId.value, {
|
||||
title: form.title,
|
||||
content: form.content,
|
||||
pinned: form.pinned
|
||||
})
|
||||
} else {
|
||||
await createNote({
|
||||
title: form.title,
|
||||
content: form.content,
|
||||
pinned: form.pinned
|
||||
})
|
||||
}
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 400)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onDelete() {
|
||||
uni.showModal({
|
||||
title: '删除笔记',
|
||||
content: '删除后无法恢复,确定继续吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await deleteNote(noteId.value)
|
||||
uni.showToast({ title: '已删除', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 400)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
padding: 24rpx $page-padding-x;
|
||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.form-card {
|
||||
@include card;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.field {
|
||||
padding: 24rpx 28rpx;
|
||||
|
||||
& + & {
|
||||
border-top: 1rpx solid $color-divider;
|
||||
}
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.field-input,
|
||||
.field-textarea {
|
||||
width: 100%;
|
||||
font-size: 30rpx;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-field {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.field-textarea {
|
||||
height: 480rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
display: block;
|
||||
text-align: right;
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.switch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx;
|
||||
border-top: 1rpx solid $color-divider;
|
||||
}
|
||||
|
||||
.switch-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
font-size: 28rpx;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.switch-dot {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 50%;
|
||||
border: 1rpx solid $color-border;
|
||||
background: $color-card;
|
||||
|
||||
&.on {
|
||||
background: $color-primary;
|
||||
border-color: $color-primary;
|
||||
box-shadow: inset 0 0 0 6rpx #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin-top: 20rpx;
|
||||
padding: 0 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 20rpx $page-padding-x;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: $color-card;
|
||||
border-top: 1rpx solid $color-border;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-danger {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: $radius-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: $color-primary;
|
||||
color: #fff;
|
||||
|
||||
&:active {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: $color-bg-page;
|
||||
color: #f56c6c;
|
||||
border: 1rpx solid #fde2e2;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="toolbar">
|
||||
<view class="search-bar">
|
||||
<FaIcon name="magnifying-glass" color="#909399" :size="16" />
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
placeholder="搜索笔记标题或内容"
|
||||
placeholder-class="search-placeholder"
|
||||
confirm-type="search"
|
||||
@confirm="loadNotes"
|
||||
/>
|
||||
<view v-if="keyword" class="search-clear" @tap="clearSearch">
|
||||
<FaIcon name="circle-xmark" color="#c0c4cc" :size="16" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="stats-row">
|
||||
<text class="stats-text">共 {{ total }} 条</text>
|
||||
<text v-if="pinnedCount" class="stats-text">置顶 {{ pinnedCount }} 条</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="list-scroll" scroll-y :show-scrollbar="false">
|
||||
<view v-if="loading" class="state-box">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="notes.length === 0" class="state-box">
|
||||
<FaIcon name="note-sticky" color="#c0c4cc" :size="40" />
|
||||
<text class="state-text">{{ keyword ? '未找到相关笔记' : '暂无笔记,点击右下角新建' }}</text>
|
||||
</view>
|
||||
<view v-else class="note-list">
|
||||
<view
|
||||
v-for="item in notes"
|
||||
:key="item.id"
|
||||
class="note-card"
|
||||
@tap="openEdit(item.id)"
|
||||
>
|
||||
<view class="note-head">
|
||||
<view class="note-title-row">
|
||||
<text v-if="item.pinned" class="pin-tag">置顶</text>
|
||||
<text class="note-title">{{ item.title }}</text>
|
||||
</view>
|
||||
<text class="note-time">{{ formatRelativeTime(item.updatedAt) }}</text>
|
||||
</view>
|
||||
<text class="note-preview">{{ preview(item.content) }}</text>
|
||||
<view class="note-actions" @tap.stop>
|
||||
<view class="action-btn" @tap="onTogglePin(item)">
|
||||
<FaIcon :name="item.pinned ? 'thumbtack' : 'thumbtack'" :color="item.pinned ? '#3c9cff' : '#909399'" :size="14" />
|
||||
<text>{{ item.pinned ? '取消置顶' : '置顶' }}</text>
|
||||
</view>
|
||||
<view class="action-btn danger" @tap="onDelete(item)">
|
||||
<FaIcon name="trash-can" color="#f56c6c" :size="14" />
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bottom-space" />
|
||||
</scroll-view>
|
||||
|
||||
<view class="fab" @tap="createNote">
|
||||
<FaIcon name="plus" color="#ffffff" :size="22" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { fetchNotes, deleteNote, toggleNotePin } from '@/api/note.js'
|
||||
import { formatRelativeTime } from '@/utils/date.js'
|
||||
import { isLoggedIn } from '@/utils/auth.js'
|
||||
|
||||
const keyword = ref('')
|
||||
const notes = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const pinnedCount = computed(() => notes.value.filter(item => item.pinned).length)
|
||||
|
||||
onMounted(() => {
|
||||
if (!isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
loadNotes()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (isLoggedIn()) loadNotes()
|
||||
})
|
||||
|
||||
async function loadNotes() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchNotes({ keyword: keyword.value })
|
||||
notes.value = res.list
|
||||
total.value = res.total
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
keyword.value = ''
|
||||
loadNotes()
|
||||
}
|
||||
|
||||
function preview(content) {
|
||||
const text = (content || '').replace(/\s+/g, ' ').trim()
|
||||
if (!text) return '暂无内容'
|
||||
return text.length > 60 ? `${text.slice(0, 60)}...` : text
|
||||
}
|
||||
|
||||
function createNote() {
|
||||
uni.navigateTo({ url: '/pages/tools/notepad/edit' })
|
||||
}
|
||||
|
||||
function openEdit(id) {
|
||||
uni.navigateTo({ url: `/pages/tools/notepad/edit?id=${id}` })
|
||||
}
|
||||
|
||||
async function onTogglePin(item) {
|
||||
try {
|
||||
await toggleNotePin(item.id)
|
||||
await loadNotes()
|
||||
uni.showToast({ title: item.pinned ? '已取消置顶' : '已置顶', icon: 'none' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function onDelete(item) {
|
||||
uni.showModal({
|
||||
title: '删除笔记',
|
||||
content: `确定删除「${item.title}」吗?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await deleteNote(item.id)
|
||||
await loadNotes()
|
||||
uni.showToast({ title: '已删除', icon: 'none' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
flex-shrink: 0;
|
||||
padding: 20rpx $page-padding-x 0;
|
||||
background: $color-card;
|
||||
box-shadow: $shadow-header;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-full;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
font-size: 28rpx;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: $color-text-muted;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
display: flex;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 16rpx 4rpx 20rpx;
|
||||
}
|
||||
|
||||
.stats-text {
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.list-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.note-list {
|
||||
padding: 24rpx $page-padding-x 0;
|
||||
}
|
||||
|
||||
.note-card {
|
||||
@include card;
|
||||
padding: 28rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&:active {
|
||||
opacity: 0.92;
|
||||
}
|
||||
}
|
||||
|
||||
.note-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.note-title-row {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.pin-tag {
|
||||
flex-shrink: 0;
|
||||
font-size: 20rpx;
|
||||
color: $color-primary;
|
||||
background: $color-primary-bg;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
|
||||
.note-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.note-time {
|
||||
flex-shrink: 0;
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.note-preview {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-secondary;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.note-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid $color-divider;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
|
||||
&.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
.state-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 160rpx 48rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
width: 104rpx;
|
||||
height: 104rpx;
|
||||
border-radius: 50%;
|
||||
background: $color-primary;
|
||||
box-shadow: 0 8rpx 24rpx rgba(60, 156, 255, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-space {
|
||||
height: 160rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view v-if="loading" class="state-box">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
<template v-else-if="item">
|
||||
<view class="detail-card">
|
||||
<view class="content-block">
|
||||
<view class="content-header">
|
||||
<text class="info-label">日程内容</text>
|
||||
<view class="status-tag" :class="item.completed ? 'done' : 'pending'">
|
||||
<text>{{ item.completed ? '已完成' : '待办' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="content-text">{{ item.content || '无内容' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">发生时间</text>
|
||||
<text class="info-value">{{ formatDate(item.datetime, true) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row channel-row">
|
||||
<text class="info-label">提醒渠道</text>
|
||||
<view class="channel-tags">
|
||||
<text
|
||||
v-for="channel in item.remindChannels"
|
||||
:key="channel"
|
||||
class="channel-tag"
|
||||
>{{ getRemindChannelLabel(channel) }}</text>
|
||||
<text v-if="!item.remindChannels?.length" class="info-value muted">未设置</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">提前提醒时间</text>
|
||||
<text class="info-value">{{ remindText }}</text>
|
||||
</view>
|
||||
|
||||
<view class="meta-row">
|
||||
<text>创建于 {{ formatDate(item.createdAt, true) }}</text>
|
||||
<text v-if="item.updatedAt !== item.createdAt">更新于 {{ formatDate(item.updatedAt, true) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-if="item" class="footer">
|
||||
<view class="btn-complete" @tap="onToggleComplete">
|
||||
<FaIcon :name="item.completed ? 'rotate-left' : 'check'" :color="item.completed ? '#606266' : '#3c9cff'" :size="16" />
|
||||
<text>{{ item.completed ? '标为待办' : '标记完成' }}</text>
|
||||
</view>
|
||||
<view v-if="!item.completed" class="btn-edit" @tap="openEdit">
|
||||
<FaIcon name="pen-to-square" color="#3c9cff" :size="16" />
|
||||
<text>编辑</text>
|
||||
</view>
|
||||
<view class="btn-delete" @tap="onDelete">
|
||||
<FaIcon name="trash-can" color="#f56c6c" :size="16" />
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import {
|
||||
getSchedule,
|
||||
deleteSchedule,
|
||||
toggleScheduleComplete,
|
||||
getRemindChannelLabel,
|
||||
getRemindMinutesLabel
|
||||
} from '@/api/schedule.js'
|
||||
import { formatDate } from '@/utils/date.js'
|
||||
|
||||
const scheduleId = ref('')
|
||||
const item = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const remindText = computed(() => {
|
||||
if (!item.value) return ''
|
||||
return getRemindMinutesLabel(item.value.remindMinutes)
|
||||
})
|
||||
|
||||
onLoad((query) => {
|
||||
if (query?.id) {
|
||||
scheduleId.value = query.id
|
||||
loadDetail()
|
||||
}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (scheduleId.value) loadDetail()
|
||||
})
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await getSchedule(scheduleId.value)
|
||||
if (!data) {
|
||||
uni.showToast({ title: '日程不存在', icon: 'none' })
|
||||
setTimeout(() => uni.navigateBack(), 800)
|
||||
return
|
||||
}
|
||||
item.value = data
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit() {
|
||||
uni.navigateTo({ url: `/pages/tools/schedule/edit?id=${scheduleId.value}` })
|
||||
}
|
||||
|
||||
async function onToggleComplete() {
|
||||
if (!item.value) return
|
||||
try {
|
||||
const updated = await toggleScheduleComplete(item.value.id)
|
||||
item.value = updated
|
||||
uni.showToast({
|
||||
title: updated.completed ? '已标记完成' : '已标为待办',
|
||||
icon: 'none'
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function onDelete() {
|
||||
const summary = (item.value?.content || '').split('\n')[0] || '该日程'
|
||||
uni.showModal({
|
||||
title: '删除日程',
|
||||
content: `确定删除「${summary}」吗?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await deleteSchedule(scheduleId.value)
|
||||
uni.showToast({ title: '已删除', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 400)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
padding: 24rpx $page-padding-x;
|
||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
@include card;
|
||||
padding: 32rpx 28rpx;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 24rpx;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.info-label {
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: $radius-sm;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
|
||||
&.pending {
|
||||
background: $color-primary-bg;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
&.done {
|
||||
background: #f0f9eb;
|
||||
color: #67c23a;
|
||||
}
|
||||
}
|
||||
|
||||
.content-text {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
color: $color-text;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24rpx;
|
||||
padding: 20rpx 0;
|
||||
border-top: 1rpx solid $color-divider;
|
||||
}
|
||||
|
||||
.channel-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 28rpx;
|
||||
color: $color-text;
|
||||
text-align: right;
|
||||
|
||||
&.muted {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.channel-tag {
|
||||
font-size: 24rpx;
|
||||
color: $color-primary;
|
||||
background: $color-primary-bg;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
padding-top: 24rpx;
|
||||
margin-top: 8rpx;
|
||||
border-top: 1rpx solid $color-divider;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx $page-padding-x;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: $color-card;
|
||||
border-top: 1rpx solid $color-border;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.btn-complete,
|
||||
.btn-edit,
|
||||
.btn-delete {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: $radius-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-complete {
|
||||
background: $color-primary-bg;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: $color-bg-page;
|
||||
color: $color-primary;
|
||||
border: 1rpx solid $color-primary-bg;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: $color-bg-page;
|
||||
color: #f56c6c;
|
||||
border: 1rpx solid #fde2e2;
|
||||
}
|
||||
|
||||
.state-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 160rpx 0;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
font-size: 28rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,375 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view v-if="isCompleted" class="completed-notice">
|
||||
<FaIcon name="circle-check" color="#67c23a" :size="40" />
|
||||
<text>该日程已完成,不可编辑</text>
|
||||
</view>
|
||||
|
||||
<view class="form-card" :class="{ 'is-disabled': isCompleted }">
|
||||
<view class="field">
|
||||
<text class="field-label">日程内容 <text class="required">*</text></text>
|
||||
<textarea
|
||||
class="field-textarea content-textarea"
|
||||
v-model="form.content"
|
||||
placeholder="输入日程内容,支持多行"
|
||||
placeholder-class="placeholder"
|
||||
maxlength="1000"
|
||||
:show-confirm-bar="false"
|
||||
:disabled="isCompleted"
|
||||
/>
|
||||
<text class="char-count">{{ form.content.length }}/1000</text>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">发生日期 <text class="required">*</text></text>
|
||||
<picker mode="date" :value="form.date" @change="onDateChange" :disabled="isCompleted">
|
||||
<view class="picker-value">
|
||||
<text>{{ form.date || '选择日期' }}</text>
|
||||
<FaIcon name="calendar-days" color="#909399" :size="16" />
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">发生时间 <text class="required">*</text></text>
|
||||
<picker mode="time" :value="form.time" @change="onTimeChange" :disabled="isCompleted">
|
||||
<view class="picker-value">
|
||||
<text>{{ form.time || '选择时间' }}</text>
|
||||
<FaIcon name="clock" color="#909399" :size="16" />
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="field-label">提醒渠道</text>
|
||||
<text class="field-hint">可多选,接入接口后按渠道下发提醒</text>
|
||||
<view class="channel-grid">
|
||||
<view
|
||||
v-for="channel in channelOptions"
|
||||
:key="channel.value"
|
||||
class="channel-chip"
|
||||
:class="{ active: form.remindChannels.includes(channel.value) }"
|
||||
@tap="!isCompleted && toggleChannel(channel.value)"
|
||||
>
|
||||
<text>{{ channel.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="field picker-field">
|
||||
<text class="field-label">提前提醒时间</text>
|
||||
<picker :range="remindLabels" :value="remindIndex" @change="onRemindChange" :disabled="isCompleted">
|
||||
<view class="picker-value">
|
||||
<text>{{ remindLabels[remindIndex] }}</text>
|
||||
<FaIcon name="bell" color="#909399" :size="16" />
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!isCompleted" class="footer">
|
||||
<view class="btn-primary full" :class="{ disabled: saving }" @tap="onSave">
|
||||
{{ saving ? '保存中...' : '保存' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import {
|
||||
getSchedule,
|
||||
createSchedule,
|
||||
updateSchedule,
|
||||
REMIND_CHANNEL_OPTIONS,
|
||||
REMIND_MINUTES_OPTIONS
|
||||
} from '@/api/schedule.js'
|
||||
import { formatDate, pad } from '@/utils/date.js'
|
||||
|
||||
const scheduleId = ref('')
|
||||
const saving = ref(false)
|
||||
const isCompleted = ref(false)
|
||||
const channelOptions = REMIND_CHANNEL_OPTIONS
|
||||
const remindOptions = REMIND_MINUTES_OPTIONS
|
||||
const remindLabels = remindOptions.map(item => item.label)
|
||||
|
||||
const form = reactive({
|
||||
content: '',
|
||||
date: '',
|
||||
time: '',
|
||||
remindChannels: ['app'],
|
||||
remindMinutes: 15
|
||||
})
|
||||
|
||||
const remindIndex = computed(() => {
|
||||
const index = remindOptions.findIndex(item => item.value === form.remindMinutes)
|
||||
return index >= 0 ? index : 3
|
||||
})
|
||||
|
||||
onLoad(async (query) => {
|
||||
const now = new Date()
|
||||
form.date = formatDate(now)
|
||||
form.time = `${pad(now.getHours())}:${pad(now.getMinutes())}`
|
||||
|
||||
if (query?.id) {
|
||||
scheduleId.value = query.id
|
||||
uni.setNavigationBarTitle({ title: '编辑日程' })
|
||||
await loadSchedule(query.id)
|
||||
} else {
|
||||
uni.setNavigationBarTitle({ title: '新建日程' })
|
||||
}
|
||||
})
|
||||
|
||||
async function loadSchedule(id) {
|
||||
try {
|
||||
const item = await getSchedule(id)
|
||||
if (!item) {
|
||||
uni.showToast({ title: '日程不存在', icon: 'none' })
|
||||
setTimeout(() => uni.navigateBack(), 800)
|
||||
return
|
||||
}
|
||||
const d = new Date(item.datetime)
|
||||
form.content = item.content || ''
|
||||
form.date = formatDate(d)
|
||||
form.time = `${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
form.remindChannels = [...(item.remindChannels || [])]
|
||||
form.remindMinutes = item.remindMinutes ?? 15
|
||||
isCompleted.value = !!item.completed
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function toggleChannel(value) {
|
||||
const index = form.remindChannels.indexOf(value)
|
||||
if (index >= 0) {
|
||||
form.remindChannels.splice(index, 1)
|
||||
} else {
|
||||
form.remindChannels.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
function onDateChange(e) {
|
||||
form.date = e.detail.value
|
||||
}
|
||||
|
||||
function onTimeChange(e) {
|
||||
form.time = e.detail.value
|
||||
}
|
||||
|
||||
function onRemindChange(e) {
|
||||
const index = Number(e.detail.value)
|
||||
form.remindMinutes = remindOptions[index].value
|
||||
}
|
||||
|
||||
function buildDatetime() {
|
||||
const [year, month, day] = form.date.split('-').map(Number)
|
||||
const [hour, minute] = form.time.split(':').map(Number)
|
||||
return new Date(year, month - 1, day, hour, minute).getTime()
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (saving.value) return
|
||||
if (!form.content.trim()) {
|
||||
uni.showToast({ title: '请输入日程内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!form.date || !form.time) {
|
||||
uni.showToast({ title: '请选择发生时间', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const datetime = buildDatetime()
|
||||
if (datetime < Date.now() - 60000 && !scheduleId.value) {
|
||||
uni.showModal({
|
||||
title: '时间提示',
|
||||
content: '发生时间已过去,仍要保存吗?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) await saveData(datetime)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
await saveData(datetime)
|
||||
}
|
||||
|
||||
async function saveData(datetime) {
|
||||
saving.value = true
|
||||
const payload = {
|
||||
content: form.content,
|
||||
datetime,
|
||||
remindChannels: [...form.remindChannels],
|
||||
remindMinutes: form.remindMinutes
|
||||
}
|
||||
try {
|
||||
if (scheduleId.value) {
|
||||
await updateSchedule(scheduleId.value, payload)
|
||||
} else {
|
||||
await createSchedule(payload)
|
||||
}
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 400)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
padding: 24rpx $page-padding-x;
|
||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.completed-notice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
padding: 20rpx 0;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #67c23a;
|
||||
background: #f0f9eb;
|
||||
border-radius: $radius-md;
|
||||
}
|
||||
|
||||
.is-disabled {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
@include card;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.field {
|
||||
padding: 24rpx 28rpx;
|
||||
|
||||
& + & {
|
||||
border-top: 1rpx solid $color-divider;
|
||||
}
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.field-textarea {
|
||||
width: 100%;
|
||||
font-size: 30rpx;
|
||||
color: $color-text;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.content-textarea {
|
||||
height: 240rpx;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
display: block;
|
||||
text-align: right;
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 72rpx;
|
||||
padding: 0 20rpx;
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-md;
|
||||
font-size: 28rpx;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.channel-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.channel-chip {
|
||||
padding: 14rpx 28rpx;
|
||||
border-radius: $radius-full;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-secondary;
|
||||
background: $color-bg-page;
|
||||
border: 1rpx solid transparent;
|
||||
|
||||
&.active {
|
||||
color: $color-primary;
|
||||
background: $color-primary-bg;
|
||||
border-color: $color-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 20rpx $page-padding-x;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: $color-card;
|
||||
border-top: 1rpx solid $color-border;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: $radius-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
background: $color-primary;
|
||||
color: #fff;
|
||||
|
||||
&:active {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.full {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,428 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="toolbar">
|
||||
<view class="search-bar">
|
||||
<FaIcon name="magnifying-glass" color="#909399" :size="16" />
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
placeholder="搜索日程内容"
|
||||
placeholder-class="search-placeholder"
|
||||
confirm-type="search"
|
||||
@confirm="loadSchedules"
|
||||
/>
|
||||
<view v-if="keyword" class="search-clear" @tap="clearSearch">
|
||||
<FaIcon name="circle-xmark" color="#c0c4cc" :size="16" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
class="tab"
|
||||
:class="{ active: status === tab.value }"
|
||||
@tap="changeStatus(tab.value)"
|
||||
>{{ tab.label }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="list-scroll" scroll-y :show-scrollbar="false">
|
||||
<view v-if="loading" class="state-box">
|
||||
<text class="state-text">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="groupedList.length === 0" class="state-box">
|
||||
<FaIcon name="calendar-days" color="#c0c4cc" :size="40" />
|
||||
<text class="state-text">{{ keyword ? '未找到相关日程' : '暂无日程,点击右下角新建' }}</text>
|
||||
</view>
|
||||
<view v-else class="schedule-list">
|
||||
<view v-for="group in groupedList" :key="group.key" class="group">
|
||||
<text class="group-title">{{ group.label }}</text>
|
||||
<view
|
||||
v-for="item in group.items"
|
||||
:key="item.id"
|
||||
class="schedule-card"
|
||||
:class="{ done: item.completed }"
|
||||
>
|
||||
<view class="schedule-main" @tap="openDetail(item.id)">
|
||||
<view class="schedule-head">
|
||||
<view class="status-dot" :class="item.completed ? 'done' : 'pending'" />
|
||||
<text class="schedule-title">{{ getScheduleSummary(item) }}</text>
|
||||
</view>
|
||||
<view class="schedule-meta">
|
||||
<FaIcon name="clock" color="#909399" :size="12" />
|
||||
<text>{{ formatDate(item.datetime, true) }}</text>
|
||||
</view>
|
||||
<view v-if="item.remindChannels?.length" class="channel-row">
|
||||
<text
|
||||
v-for="ch in item.remindChannels"
|
||||
:key="ch"
|
||||
class="channel-mini"
|
||||
>{{ getRemindChannelLabel(ch) }}</text>
|
||||
<text v-if="item.remindMinutes" class="remind-text">{{ getRemindMinutesLabel(item.remindMinutes) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-row">
|
||||
<view
|
||||
class="action-btn"
|
||||
:class="{ disabled: item.completed }"
|
||||
@tap="!item.completed && onToggleComplete(item)"
|
||||
>
|
||||
<FaIcon name="check" :color="item.completed ? '#c0c4cc' : '#3c9cff'" :size="14" />
|
||||
<text>{{ item.completed ? '已完成' : '标记完成' }}</text>
|
||||
</view>
|
||||
<view v-if="!item.completed" class="action-btn" @tap="openEdit(item.id)">
|
||||
<FaIcon name="pen-to-square" color="#3c9cff" :size="14" />
|
||||
<text>编辑</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bottom-space" />
|
||||
</scroll-view>
|
||||
|
||||
<view class="fab" @tap="createSchedule">
|
||||
<FaIcon name="plus" color="#ffffff" :size="22" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { fetchSchedules, toggleScheduleComplete, getScheduleSummary, getRemindChannelLabel, getRemindMinutesLabel } from '@/api/schedule.js'
|
||||
import { formatDate, dayLabel, isToday, startOfDay } from '@/utils/date.js'
|
||||
import { isLoggedIn } from '@/utils/auth.js'
|
||||
|
||||
const keyword = ref('')
|
||||
const status = ref('all')
|
||||
const schedules = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const tabs = [
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'pending', label: '待办' },
|
||||
{ value: 'done', label: '已完成' }
|
||||
]
|
||||
|
||||
const groupedList = computed(() => {
|
||||
const map = new Map()
|
||||
schedules.value.forEach(item => {
|
||||
const key = startOfDay(item.datetime).getTime()
|
||||
if (!map.has(key)) {
|
||||
map.set(key, {
|
||||
key,
|
||||
label: dayLabel(item.datetime),
|
||||
isToday: isToday(item.datetime),
|
||||
items: []
|
||||
})
|
||||
}
|
||||
map.get(key).items.push(item)
|
||||
})
|
||||
return [...map.values()].sort((a, b) => a.key - b.key)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!isLoggedIn()) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
loadSchedules()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (isLoggedIn()) loadSchedules()
|
||||
})
|
||||
|
||||
async function loadSchedules() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchSchedules({
|
||||
keyword: keyword.value,
|
||||
status: status.value
|
||||
})
|
||||
schedules.value = res.list
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
keyword.value = ''
|
||||
loadSchedules()
|
||||
}
|
||||
|
||||
function changeStatus(value) {
|
||||
status.value = value
|
||||
loadSchedules()
|
||||
}
|
||||
|
||||
function createSchedule() {
|
||||
uni.navigateTo({ url: '/pages/tools/schedule/edit' })
|
||||
}
|
||||
|
||||
function openDetail(id) {
|
||||
uni.navigateTo({ url: `/pages/tools/schedule/detail?id=${id}` })
|
||||
}
|
||||
|
||||
function openEdit(id) {
|
||||
uni.navigateTo({ url: `/pages/tools/schedule/edit?id=${id}` })
|
||||
}
|
||||
|
||||
async function onToggleComplete(item) {
|
||||
try {
|
||||
await toggleScheduleComplete(item.id)
|
||||
await loadSchedules()
|
||||
uni.showToast({
|
||||
title: item.completed ? '已标为待办' : '已标记完成',
|
||||
icon: 'none'
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/src/styles/page-common.scss';
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
flex-shrink: 0;
|
||||
padding: 20rpx $page-padding-x 0;
|
||||
background: $color-card;
|
||||
box-shadow: $shadow-header;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
background: $color-bg-page;
|
||||
border-radius: $radius-full;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
font-size: 28rpx;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: $color-text-muted;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
display: flex;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx 0 24rpx;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10rpx 28rpx;
|
||||
border-radius: $radius-full;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-secondary;
|
||||
background: $color-bg-page;
|
||||
|
||||
&.active {
|
||||
color: $color-primary;
|
||||
background: $color-primary-bg;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.list-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.schedule-list {
|
||||
padding: 24rpx $page-padding-x 0;
|
||||
}
|
||||
|
||||
.group {
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-secondary;
|
||||
margin-bottom: 16rpx;
|
||||
padding-left: 4rpx;
|
||||
}
|
||||
|
||||
.schedule-card {
|
||||
@include card;
|
||||
margin-bottom: 16rpx;
|
||||
overflow: hidden;
|
||||
|
||||
&.done .schedule-title {
|
||||
text-decoration: line-through;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-main {
|
||||
padding: 24rpx 24rpx 16rpx;
|
||||
|
||||
&:active {
|
||||
background: $color-bg-page;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.pending {
|
||||
background: $color-primary;
|
||||
}
|
||||
|
||||
&.done {
|
||||
background: #67c23a;
|
||||
}
|
||||
}
|
||||
|
||||
.schedule-title {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.schedule-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
padding-left: 24rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.channel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
padding-left: 24rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.channel-mini {
|
||||
font-size: 22rpx;
|
||||
color: $color-primary;
|
||||
background: $color-primary-bg;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
|
||||
.remind-text {
|
||||
font-size: 22rpx;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
border-top: 1rpx solid $color-divider;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 20rpx 0;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-muted;
|
||||
|
||||
& + & {
|
||||
border-left: 1rpx solid $color-divider;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $color-bg-page;
|
||||
}
|
||||
}
|
||||
|
||||
.state-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 160rpx 48rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
font-size: 26rpx;
|
||||
color: $color-text-muted;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
width: 104rpx;
|
||||
height: 104rpx;
|
||||
border-radius: 50%;
|
||||
background: $color-primary;
|
||||
box-shadow: 0 8rpx 24rpx rgba(60, 156, 255, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-space {
|
||||
height: 160rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
@import '@/src/styles/theme.scss';
|
||||
|
||||
@mixin page-shell {
|
||||
height: 100vh;
|
||||
background: $color-bg-page;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@mixin scroll-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 24rpx $page-padding-x 0;
|
||||
}
|
||||
|
||||
@mixin card {
|
||||
background: $color-card;
|
||||
border-radius: $radius-lg;
|
||||
box-shadow: $shadow-card;
|
||||
}
|
||||
|
||||
@mixin section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text;
|
||||
}
|
||||
|
||||
@mixin icon-box($size: 72rpx) {
|
||||
width: $size;
|
||||
height: $size;
|
||||
border-radius: $radius-md;
|
||||
background: $color-primary-bg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
// 简约潮流主题变量
|
||||
$color-primary: #6366f1;
|
||||
$color-primary-light: #818cf8;
|
||||
$color-primary-dark: #4f46e5;
|
||||
$color-accent: #ec4899;
|
||||
$color-bg: #f8fafc;
|
||||
// 标准商务主题(单色蓝 + 灰阶,无渐变)
|
||||
$color-primary: #3c9cff;
|
||||
$color-primary-dark: #2b8ae8;
|
||||
$color-primary-bg: #ecf5ff;
|
||||
$color-bg-page: #f5f7fa;
|
||||
$color-card: #ffffff;
|
||||
$color-text: #1e293b;
|
||||
$color-text-secondary: #64748b;
|
||||
$color-text-muted: #94a3b8;
|
||||
$color-border: #e2e8f0;
|
||||
$gradient-primary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #ec4899 100%);
|
||||
$gradient-soft: linear-gradient(160deg, #eef2ff 0%, #faf5ff 50%, #fdf2f8 100%);
|
||||
$shadow-sm: 0 2rpx 12rpx rgba(99, 102, 241, 0.08);
|
||||
$shadow-md: 0 8rpx 32rpx rgba(99, 102, 241, 0.12);
|
||||
$radius-sm: 16rpx;
|
||||
$radius-md: 24rpx;
|
||||
$radius-lg: 32rpx;
|
||||
$color-text: #303133;
|
||||
$color-text-secondary: #606266;
|
||||
$color-text-muted: #909399;
|
||||
$color-border: #ebeef5;
|
||||
$color-divider: #f2f3f5;
|
||||
$shadow-card: 0 4rpx 24rpx rgba(0, 0, 0, 0.05);
|
||||
$shadow-header: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
$radius-sm: 8rpx;
|
||||
$radius-md: 12rpx;
|
||||
$radius-lg: 16rpx;
|
||||
$radius-full: 999rpx;
|
||||
$page-padding-x: 32rpx;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
export function pad(n) {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
export function toDate(value) {
|
||||
if (!value) return null
|
||||
const d = value instanceof Date ? value : new Date(value)
|
||||
return Number.isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
export function formatDate(value, withTime = false) {
|
||||
const d = toDate(value)
|
||||
if (!d) return ''
|
||||
const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
||||
if (!withTime) return date
|
||||
return `${date} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
export function formatRelativeTime(value) {
|
||||
const d = toDate(value)
|
||||
if (!d) return ''
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - d.getTime()
|
||||
const minute = 60 * 1000
|
||||
const hour = 60 * minute
|
||||
const day = 24 * hour
|
||||
|
||||
if (diff < minute) return '刚刚'
|
||||
if (diff < hour) return `${Math.floor(diff / minute)} 分钟前`
|
||||
if (diff < day) return `${Math.floor(diff / hour)} 小时前`
|
||||
if (diff < 7 * day) return `${Math.floor(diff / day)} 天前`
|
||||
return formatDate(d)
|
||||
}
|
||||
|
||||
export function isSameDay(a, b) {
|
||||
const da = toDate(a)
|
||||
const db = toDate(b)
|
||||
if (!da || !db) return false
|
||||
return (
|
||||
da.getFullYear() === db.getFullYear() &&
|
||||
da.getMonth() === db.getMonth() &&
|
||||
da.getDate() === db.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
export function isToday(value) {
|
||||
return isSameDay(value, new Date())
|
||||
}
|
||||
|
||||
export function isTomorrow(value) {
|
||||
const d = toDate(value)
|
||||
if (!d) return false
|
||||
const tomorrow = new Date()
|
||||
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||
return isSameDay(d, tomorrow)
|
||||
}
|
||||
|
||||
export function dayLabel(value) {
|
||||
if (isToday(value)) return '今天'
|
||||
if (isTomorrow(value)) return '明天'
|
||||
return formatDate(value)
|
||||
}
|
||||
|
||||
export function startOfDay(value = new Date()) {
|
||||
const d = toDate(value) || new Date()
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||||
}
|
||||
|
||||
export function endOfDay(value = new Date()) {
|
||||
const d = startOfDay(value)
|
||||
d.setHours(23, 59, 59, 999)
|
||||
return d
|
||||
}
|
||||
Reference in New Issue
Block a user