This commit is contained in:
lik
2026-06-08 12:01:40 +08:00
parent 010cf160a0
commit 894a9881d7
51 changed files with 2667 additions and 740 deletions

20
app.js
View File

@@ -1,6 +1,7 @@
// app.js
import config from './config';
import createBus from './utils/eventBus';
const API = require('./utils/api.js');
App({
onLaunch() {
@@ -26,6 +27,25 @@ App({
this.connect();
},
onShow(options) {
wx.login({
success: (res) => {
if (res.code) {
API.user.wxSignin({ code: res.code })
.then((data) => {
if (data.code == 0) {
this.globalData.user = data.data.user
this.eventBus.emit('user-login', data.data.user)
} else {
console.log('登录失败!')
}
})
} else {
}
}
})
},
/** 全局事件总线 */
eventBus: createBus(),

View File

@@ -12,15 +12,15 @@
"subpackages": [],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#1a1f3c",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTitleText": "Weixin",
"navigationBarTextStyle": "white",
"navigationBarTextStyle": "black",
"backgroundColor": "#0f1535"
},
"tabBar": {
"color": "#8a8a8a",
"selectedColor": "#1296db",
"backgroundColor": "#0f1535",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{

View File

@@ -0,0 +1,13 @@
import { SuperComponent, ComponentsOptionsType } from '../../../../components/common/src/index';
export default class ChatMarkdownCode extends SuperComponent {
options: ComponentsOptionsType;
properties: {
node: {
type: ObjectConstructor;
value: () => {};
};
};
data: {
classPrefix: string;
};
}

View File

@@ -0,0 +1 @@
import{__decorate}from"tslib";import{SuperComponent,wxComponent}from"../../common/src/index";import config from"../../common/config";const{prefix:prefix}=config,name=`${prefix}-chat-markdown-code`;let ChatMarkdownCode=class extends SuperComponent{constructor(){super(...arguments),this.options={multipleSlots:!0},this.properties={node:{type:Object,value:()=>({})}},this.data={classPrefix:name}}};ChatMarkdownCode=__decorate([wxComponent()],ChatMarkdownCode);export default ChatMarkdownCode;

View File

@@ -0,0 +1 @@
{"component":true,"styleIsolation":"apply-shared","usingComponents":{}}

View File

@@ -0,0 +1 @@
<view class="{{classPrefix}}"><view class="{{classPrefix}}__header" wx:if="{{node.lang}}"><text class="{{classPrefix}}__lang">{{node.lang}}</text></view><scroll-view class="{{classPrefix}}__content" scroll-x="{{true}}"><text class="{{classPrefix}}__text" decode="{{true}}">{{node.text}}</text></scroll-view></view>

View File

@@ -0,0 +1,7 @@
@import '../../common/style/index.wxss';.t-chat-markdown-code{margin:16rpx 0;border-radius:8rpx;background-color:#f6f8fa;border:1rpx solid #e1e4e8;overflow:hidden;}
.t-chat-markdown-code__header{padding:8rpx 16rpx;background-color:#e1e4e8;border-bottom:1rpx solid #d0d7de;}
.t-chat-markdown-code__lang{font-size:24rpx;color:#656d76;font-weight:500;}
.t-chat-markdown-code__content{padding:16rpx;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;white-space:nowrap;height:auto;}
.t-chat-markdown-code__text{font:var(--td-font-body-medium,28rpx / 44rpx var(--td-font-family,PingFang SC,Microsoft YaHei,Arial Regular));font-family:'SF Mono',Monaco,'Cascadia Code','Roboto Mono',Consolas,'Courier New',monospace;color:#24292f;white-space:pre;word-wrap:normal;word-break:normal;overflow-wrap:normal;display:inline-block;vertical-align:top;min-width:100%;}
.t-chat-markdown-code-light .t-chat-markdown-code-block{background-color:#fff;border-color:#d0d7de;}
.t-chat-markdown-code-light .t-chat-markdown-code-header{background-color:#f6f8fa;border-bottom-color:#d0d7de;}

View File

@@ -0,0 +1,23 @@
import { SuperComponent, ComponentsOptionsType } from '../../../../components/common/src/index';
export default class ChatMarkdownNode extends SuperComponent {
options: ComponentsOptionsType;
properties: {
nodes: {
type: ArrayConstructor;
value: () => any[];
};
};
data: {
classPrefix: string;
};
methods: {
linkClick(e: any): void;
getCareMarkdown(): any;
handleClick(event: any, type: any, token: any): void;
};
lifetimes: {
created(): void;
attached(): void;
detached(): void;
};
}

View File

@@ -0,0 +1 @@
import{__decorate}from"tslib";import{SuperComponent,wxComponent}from"../../common/src/index";import config from"../../common/config";const{prefix:prefix}=config,name=`${prefix}-chat-markdown`;let ChatMarkdownNode=class extends SuperComponent{constructor(){super(...arguments),this.options={multipleSlots:!0},this.properties={nodes:{type:Array,value:()=>[]}},this.data={classPrefix:name},this.methods={linkClick(t){var e;const{index:a}=t.currentTarget.dataset||{},r=null===(e=this.data.nodes)||void 0===e?void 0:e[a];this.handleClick(t,"link-tap",r)},getCareMarkdown(){if(this.data.careMarkdown)return this.data.careMarkdown;for(this.setData({careMarkdown:this.selectOwnerComponent()});this.data.careMarkdown.__data__.name!==name;this.setData({careMarkdown:this.data.careMarkdown.selectOwnerComponent()}));return this.data.careMarkdown},handleClick(t,e,a){this.data.getCareMarkdown().triggerEvent("click",{event:t,node:a})}},this.lifetimes={created(){this.data.getCareMarkdown=this.getCareMarkdown.bind(this),this.data.handleClick=this.handleClick.bind(this)},attached(){},detached(){}}}};ChatMarkdownNode=__decorate([wxComponent()],ChatMarkdownNode);export default ChatMarkdownNode;

View File

@@ -0,0 +1 @@
{"component":true,"styleIsolation":"apply-shared","usingComponents":{"chat-markdown-table":"../chat-markdown-table/chat-markdown-table","chat-markdown-code":"../chat-markdown-code/chat-markdown-code","chat-markdown-node":"./chat-markdown-node"}}

View File

@@ -0,0 +1 @@
<block wx:for="{{nodes}}" wx:for-item="item" wx:for-index="i" wx:key="i"><block wx:if="{{item.type==='heading'}}"><view class="{{classPrefix}}-h {{ classPrefix}}-h{{item.depth}}"><chat-markdown-node nodes="{{item.tokens}}"/></view></block><block wx:elif="{{item.type==='list'}}"><view class="{{classPrefix}}-list {{item.ordered ? classPrefix + '-list__decimal' : ''}}" data-type="{{item.ordered}}"><block wx:for="{{item.items}}" wx:for-item="li" wx:for-index="j" wx:key="j"><view class="{{classPrefix}}-list-item"><block wx:if="{{li.tokens && li.tokens.length}}"><chat-markdown-node nodes="{{li.tokens}}"/></block><block wx:else>{{''+li.text+''}}</block></view></block></view></block><block wx:elif="{{item.type==='paragraph'}}"><view class="{{classPrefix}}-p"><chat-markdown-node nodes="{{item.tokens}}"/></view></block><block wx:elif="{{item.type==='image'}}"><view class="{{classPrefix}}-image"><image src="{{item.href}}" alt="{{item.title}}" mode="widthFix"/></view></block><block wx:elif="{{item.type==='table'}}"><chat-markdown-table node="{{item}}"/></block><block wx:elif="{{item.type==='blockquote'}}"><view class="{{classPrefix}}-blockquote"><chat-markdown-node nodes="{{item.tokens}}"/></view></block><block wx:elif="{{item.type==='code'}}"><chat-markdown-code node="{{item}}" theme="{{theme}}"/></block><block wx:elif="{{item.type==='text'}}"><view class="{{classPrefix}}-text {{classPrefix}}-inline" data-raw="{{item.raw}}"><block wx:if="{{item.tokens && item.tokens.length}}"><chat-markdown-node nodes="{{item.tokens}}"/></block><block wx:else>{{''+item.raw+''}}</block></view></block><block wx:elif="{{item.type==='strong'}}"><view class="{{classPrefix}}-strong {{classPrefix}}-inline"><block wx:if="{{item.tokens && item.tokens.length}}"><chat-markdown-node nodes="{{item.tokens}}"/></block><block wx:else>{{''+item.text+''}}</block></view></block><block wx:elif="{{item.type==='em'}}"><view class="{{classPrefix}}-em {{classPrefix}}-inline"><block wx:if="{{item.tokens && item.tokens.length}}"><chat-markdown-node nodes="{{item.tokens}}"/></block><block wx:else>{{''+item.text+''}}</block></view></block><block wx:elif="{{item.type==='del'}}"><view class="{{classPrefix}}-del {{classPrefix}}-inline"><block wx:if="{{item.tokens && item.tokens.length}}"><chat-markdown-node nodes="{{item.tokens}}"/></block><block wx:else>{{''+item.text+''}}</block></view></block><block wx:elif="{{item.type==='link'}}"><view class="{{classPrefix}}-link {{classPrefix}}-inline" data-index="{{i}}" bindtap="linkClick"><block wx:if="{{item.tokens && item.tokens.length}}"><chat-markdown-node nodes="{{item.tokens}}"/></block></view></block><block wx:elif="{{item.type==='ref'}}"><view class="{{classPrefix}}-ref {{classPrefix}}-inline"><text class="{{classPrefix}}-ref-txt">{{''+item.text+''}}</text></view></block><block wx:elif="{{item.type==='space'}}"><view class="{{classPrefix}}-space"></view></block><block wx:elif="{{item.type==='br'}}"><view class="{{classPrefix}}-br"></view></block><block wx:elif="{{item.type==='hr'}}"><view class="{{classPrefix}}-hr"></view></block><block wx:elif="{{item.type==='codespan'}}"><view class="{{classPrefix}}-codespan {{classPrefix}}-inline" data-type="{{item.type}}">{{''+(item.text||item.raw)+''}}</view></block><block wx:else><view class="{{classPrefix}}-raw {{classPrefix}}-inline" data-type="{{item.type}}">{{''+(item.text||item.raw)+''}}</view></block></block>

View File

@@ -0,0 +1 @@
@import '../../common/style/index.wxss';

View File

@@ -0,0 +1,13 @@
import { SuperComponent, ComponentsOptionsType } from '../../../../components/common/src/index';
export default class ChatMarkdownTable extends SuperComponent {
options: ComponentsOptionsType;
properties: {
node: {
type: ObjectConstructor;
value: {};
};
};
data: {
classPrefix: string;
};
}

View File

@@ -0,0 +1 @@
import{__decorate}from"tslib";import{SuperComponent,wxComponent}from"../../common/src/index";import config from"../../common/config";const{prefix:prefix}=config,name=`${prefix}-chat-markdown-table`;let ChatMarkdownTable=class extends SuperComponent{constructor(){super(...arguments),this.options={multipleSlots:!0},this.properties={node:{type:Object,value:{}}},this.data={classPrefix:name}}};ChatMarkdownTable=__decorate([wxComponent()],ChatMarkdownTable);export default ChatMarkdownTable;

View File

@@ -0,0 +1 @@
{"component":true,"styleIsolation":"apply-shared","usingComponents":{"chat-markdown-node":"../chat-markdown-node/chat-markdown-node"}}

View File

@@ -0,0 +1 @@
<view class="{{classPrefix}}"><view class="{{classPrefix}}__container"><view class="{{classPrefix}}__thead"><view class="{{classPrefix}}__tr"><block wx:for="{{node.header}}" wx:for-item="th" wx:for-index="j" wx:key="j"><view class="{{classPrefix}}__th" style="{{('text-align:'+node.align[j]||'left'+';')}}"><chat-markdown-node nodes="{{th.tokens}}"></chat-markdown-node></view></block></view></view><view class="{{classPrefix}}__tbody"><block wx:for="{{node.rows}}" wx:for-item="row" wx:for-index="k" wx:key="k"><view class="{{classPrefix}}__tr"><block wx:for="{{row}}" wx:for-item="cell" wx:for-index="l" wx:key="l"><view class="{{classPrefix}}__td" style="{{('text-align:'+node.align[l]||'left'+';')}}"><chat-markdown-node nodes="{{cell.tokens}}"></chat-markdown-node></view></block></view></block></view></view></view>

View File

@@ -0,0 +1,12 @@
@import '../../common/style/index.wxss';.t-chat-markdown-table{width:100%;overflow-y:hidden;overflow-x:auto;border:1rpx solid var(--td-component-border,var(--td-gray-color-4,#dcdcdc));}
.t-chat-markdown-table__container{display:table;min-width:100%;max-width:max-content;border-collapse:collapse;white-space:nowrap;}
.t-chat-markdown-table__thead{display:table-header-group;}
.t-chat-markdown-table__tbody{display:table-row-group;}
.t-chat-markdown-table__tr{display:table-row;border-bottom:1rpx solid var(--td-component-border,var(--td-gray-color-4,#dcdcdc));}
.t-chat-markdown-table__tr:last-child{border-bottom:none;}
.t-chat-markdown-table__tr:nth-child(2n+1){background-color:var(--td-bg-color-container,var(--td-font-white-1,#fff));}
.t-chat-markdown-table__tr:nth-child(2n){background-color:var(--td-bg-color-secondarycontainer,var(--td-gray-color-1,#f3f3f3));}
.t-chat-markdown-table__th{display:table-cell;vertical-align:middle;background-color:var(--td-bg-color-secondarycontainer,var(--td-gray-color-1,#f3f3f3));padding:5rpx 10rpx;color:var(--td-text-color-secondary,var(--td-font-gray-2,rgba(0,0,0,.6)));border-right:1rpx solid var(--td-component-border,var(--td-gray-color-4,#dcdcdc));}
.t-chat-markdown-table__th:last-child{border-right:none;}
.t-chat-markdown-table__td{display:table-cell;vertical-align:middle;padding:5rpx 10rpx;border-right:1rpx solid var(--td-component-border,var(--td-gray-color-4,#dcdcdc));}
.t-chat-markdown-table__td:last-child{border-right:none;}

View File

@@ -0,0 +1,22 @@
import { SuperComponent, ComponentsOptionsType } from '../../../components/common/src/index';
import { TdChatMarkdownProps } from './type';
export interface ChatMarkdownProps extends TdChatMarkdownProps {
}
export default class ChatMarkdown extends SuperComponent {
options: ComponentsOptionsType;
properties: TdChatMarkdownProps;
data: {
classPrefix: string;
nodes: any[];
name: string;
};
observers: {
content: (markdown: string) => void;
};
methods: {
parseMarkdown(markdown: string): void;
};
lifetimes: {
attached(): void;
};
}

View File

@@ -0,0 +1 @@
import{__decorate}from"tslib";import{Lexer}from"marked";import{SuperComponent,wxComponent}from"../common/src/index";import config from"../common/config";import props from"./props";const{prefix:prefix}=config,name=`${prefix}-chat-markdown`;let ChatMarkdown=class extends SuperComponent{constructor(){super(...arguments),this.options={multipleSlots:!0},this.properties=props,this.data={classPrefix:name,nodes:[],name:name},this.observers={content:function(o){this.parseMarkdown(o)}},this.methods={parseMarkdown(o){try{const t=new Lexer(this.data.options).lex(o);this.setData({nodes:t})}catch(t){console.error("Markdown parsing error:",t),this.setData({nodes:[{type:"text",raw:o,text:o}]})}}},this.lifetimes={attached(){}}}};ChatMarkdown=__decorate([wxComponent()],ChatMarkdown);export default ChatMarkdown;

View File

@@ -0,0 +1 @@
{"component":true,"styleIsolation":"shared","usingComponents":{"chat-markdown-node":"./chat-markdown-node/chat-markdown-node"}}

View File

@@ -0,0 +1 @@
<wxs src="../common/utils.wxs" module="_"/><view class="class {{classPrefix}} {{classPrefix}}--normal" style="{{_._style([style, customStyle])}}"><chat-markdown-node nodes="{{nodes}}"></chat-markdown-node></view>

View File

@@ -0,0 +1,28 @@
@import '../common/style/index.wxss';.t-chat-markdown{color:var(--td-text-color-primary,var(--td-font-gray-1,rgba(0,0,0,.9)));word-wrap:break-word;word-break:break-word;line-height:1.75;}
.t-chat-markdown-inline{display:inline;}
.t-chat-markdown-p{-webkit-margin-before:var(--td-spacer-1,24rpx);margin-block-start:var(--td-spacer-1,24rpx);-webkit-margin-after:var(--td-spacer-1,24rpx);margin-block-end:var(--td-spacer-1,24rpx);}
.t-chat-markdown-p:first-child{-webkit-margin-before:0;margin-block-start:0;}
.t-chat-markdown-p:last-child{-webkit-margin-after:0;margin-block-end:0;}
.t-chat-markdown-blockquote{padding:0 .75em;color:var(--td-text-color-primary,var(--td-font-gray-1,rgba(0,0,0,.9)));background-color:var(--td-bg-color-secondarycontainer,var(--td-gray-color-1,#f3f3f3));border-left:4rpx solid var(--td-component-border,var(--td-gray-color-4,#dcdcdc));margin-bottom:var(--td-spacer-1,24rpx);}
.t-chat-markdown-h{font-size:1em;margin:var(--td-spacer-1,24rpx) 0;font-weight:700;}
.t-chat-markdown-h1{font-size:2em;}
.t-chat-markdown-h2{font-size:1.75em;}
.t-chat-markdown-h3{font-size:1.5em;}
.t-chat-markdown-h4{font-size:1.25em;}
.t-chat-markdown-h5{font-size:1em;}
.t-chat-markdown-h6{font-size:.75em;}
.t-chat-markdown-em{font-style:italic;}
.t-chat-markdown-strong{font-weight:700;}
.t-chat-markdown-hr{height:6rpx;padding:0;margin:var(--td-spacer-1,24rpx) 0;background-color:var(--td-component-border,var(--td-gray-color-4,#dcdcdc));border:0;}
.t-chat-markdown-list{display:block;padding:0;margin:0 0 var(--td-spacer,16rpx) 1.5em;}
.t-chat-markdown-list__decimal{list-style-type:decimal;}
.t-chat-markdown-list-item{display:list-item;margin-bottom:var(--td-spacer-1,24rpx);}
.t-chat-markdown-link{color:var(--td-brand-color,var(--td-primary-color-7,#0052d9));}
.t-chat-markdown-del{text-decoration:line-through;}
.t-chat-markdown-codespan{padding:4rpx 8rpx;margin:0 4rpx;border-radius:8rpx;font-size:.8em;overflow-x:auto;background-color:var(--td-bg-color-page,var(--td-gray-color-1,#f3f3f3));border:1rpx solid var(--td-component-border,var(--td-gray-color-4,#dcdcdc));}
.t-chat-markdown .t-chat-markdown-table__container{display:table;width:100%;border-collapse:collapse;}
.t-chat-markdown .t-chat-markdown-table__container-thead{display:table-header-group;}
.t-chat-markdown .t-chat-markdown-table__container-tbody{display:table-row-group;}
.t-chat-markdown .t-chat-markdown-table__container-tr{display:table-row;}
.t-chat-markdown .t-chat-markdown-table__container-th{display:table-cell;vertical-align:middle;background-color:var(--td-bg-color-component,var(--td-gray-color-3,#e7e7e7));font-weight:700;padding:5rpx 10rpx;border:1rpx solid var(--td-component-border,var(--td-gray-color-4,#dcdcdc));}
.t-chat-markdown .t-chat-markdown-table__container-td{display:table-cell;vertical-align:middle;padding:5rpx 10rpx;border:1rpx solid var(--td-component-border,var(--td-gray-color-4,#dcdcdc));}

View File

@@ -0,0 +1,3 @@
export * from './props';
export * from './type';
export * from './chat-markdown';

View File

@@ -0,0 +1 @@
export*from"./props";export*from"./type";export*from"./chat-markdown";

View File

@@ -0,0 +1,3 @@
import { TdChatMarkdownProps } from './type';
declare const props: TdChatMarkdownProps;
export default props;

View File

@@ -0,0 +1 @@
const props={content:{type:String,value:"",required:!0},options:{type:Object,value:{gfm:!0,pedantic:!1,breaks:!0}}};export default props;

View File

@@ -0,0 +1,17 @@
export interface TdChatMarkdownProps {
content: {
type: StringConstructor;
value?: string;
required?: boolean;
};
options?: {
type: ObjectConstructor;
value?: TdChatContentMDOptions;
};
}
export interface TdChatContentMDOptions {
gfm?: boolean;
pedantic?: boolean;
smartLists?: boolean;
breaks?: boolean;
}

View File

@@ -0,0 +1 @@
export{};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,66 +1,237 @@
// pages/ai/index.js
const AIChatSocket = require('../../utils/chatmsg.js')
Page({
/**
* 页面的初始数据
*/
data: {
messages: [],
inputValue: '',
isTyping: false,
scrollToMessage: '',
quickQuestions: [
'今日待处理订单有哪些?',
'最近客户反馈统计',
'如何修改服务价格?',
'陪诊员排班情况'
]
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
messageIdCounter: 0,
socket: null,
onLoad() {
this.loadChatHistory()
this.initSocket()
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
if (this.socket) {
this.socket.close()
this.socket = null
}
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
initSocket() {
this.socket = new AIChatSocket()
this.socket.onOpen(() => {
console.log('[AIChat] Socket opened')
})
this.socket.onMessage((data) => {
if (data && data.type === 'ai') {
this.handleAIReply(data)
} else if (data && data.type === 'system') {
console.log('[AIChat] System:', data.content)
}
})
this.socket.onError((err) => {
console.error('[AIChat] WebSocket error', err)
this.handleAIError('网络连接失败,请检查网络设置')
})
this.socket.onClose((res) => {
console.log('[AIChat] Socket closed', res)
})
this.socket.connect().catch((err) => {
console.error('[AIChat] Connect failed', err)
})
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
handleAIReply(data) {
let messages = null
if (this.data.messages.length > 0) {
let lastMessage = this.data.messages[this.data.messages.length - 1]
if (lastMessage.type === 'ai' && lastMessage._serverId === data.id) {
lastMessage.content += data.content || ''
messages = this.data.messages
}
}
if (!messages) {
const aiResponse = {
id: `msg_${++this.messageIdCounter}`,
_serverId: data.id,
type: 'ai',
contentType: 'text',
content: data.content || '',
time: this.formatTime(new Date())
}
if (data.sessionId) {
wx.setStorageSync('admin_ai_session_id', data.sessionId)
}
messages = [...this.data.messages, aiResponse]
}
this.setData({
messages: messages,
isTyping: false
})
this.saveChatHistory(messages)
this.scrollToBottom()
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
loadChatHistory() {
try {
const history = wx.getStorageSync('admin_ai_chat_history')
if (history && history.length > 0) {
this.setData({ messages: history })
this.scrollToBottom()
}
} catch (e) {
console.log('加载聊天记录失败', e)
}
},
saveChatHistory(messages) {
try {
wx.setStorageSync('admin_ai_chat_history', messages)
} catch (e) {
console.log('保存聊天记录失败', e)
}
},
onInputChange(e) {
const value = e.detail.value
this.setData({
inputValue: value,
canSend: !!value.trim()
})
},
onQuickQuestionTap(e) {
const question = e.currentTarget.dataset.question
this.setData({ inputValue: question, canSend: true })
this.sendMessage()
},
sendMessage() {
const content = this.data.inputValue.trim()
if (!content) return
const userMessage = {
id: `msg_${++this.messageIdCounter}`,
type: 'user',
contentType: 'text',
content: content,
time: this.formatTime(new Date())
}
const messages = [...this.data.messages, userMessage]
this.setData({
messages: messages,
inputValue: '',
canSend: false,
isTyping: true
})
this.saveChatHistory(messages)
this.scrollToBottom()
this.sendToAI(content)
},
sendToAI(content, type = 'chat') {
const app = getApp()
const user = app.globalData.user
if (!this.socket) {
this.handleAIError('网络连接失败,请检查网络设置')
return
}
if (!this.socket.isConnected) {
this.socket.connect().then(() => {
this._doSend(type, content, user, app)
}).catch((err) => {
console.error('[AIChat] Reconnect failed', err)
this.handleAIError('网络连接失败,请检查网络设置')
})
} else {
this._doSend(type, content, user, app)
}
},
_doSend(type, content, user, app) {
this.socket.send({
type: type,
content: content,
userId: user ? user._id : '',
token: user ? user.security.token : '',
appId: app.globalData.appId || '',
agent: 'escort-admin'
})
},
handleAIError(errorMessage) {
const errorResponse = {
id: `msg_${++this.messageIdCounter}`,
type: 'ai',
contentType: 'text',
content: errorMessage,
time: this.formatTime(new Date())
}
const messages = [...this.data.messages, errorResponse]
this.setData({
messages: messages,
isTyping: false
})
this.saveChatHistory(messages)
this.scrollToBottom()
},
scrollToBottom() {
setTimeout(() => {
const lastMessage = this.data.messages[this.data.messages.length - 1]
if (lastMessage) {
this.setData({
scrollToMessage: `msg-${lastMessage.id}`
})
}
}, 100)
},
formatTime(date) {
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${hours}:${minutes}`
},
clearChat() {
wx.showModal({
title: '确认清空',
content: '确定要清空所有聊天记录吗?',
success: (res) => {
if (res.confirm) {
this.sendToAI('clear', 'clear')
this.setData({ messages: [] })
wx.removeStorageSync('admin_ai_chat_history')
wx.removeStorageSync('admin_ai_session_id')
}
}
})
}
})
})

View File

@@ -1,3 +1,9 @@
{
"usingComponents": {}
}
"navigationBarTitleText": "AI助手",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#ffffff",
"usingComponents": {
"t-chat-markdown": "tdesign-miniprogram/chat-markdown/chat-markdown"
}
}

View File

@@ -1,2 +1,66 @@
<!--pages/ai/index.wxml-->
<text>pages/ai/index.wxml</text>
<view class="chat-container">
<scroll-view class="message-list" scroll-y scroll-into-view="{{scrollToMessage}}" scroll-with-animation>
<view class="quick-questions" wx:if="{{messages.length === 0}}">
<view class="welcome-text">您好我是您的AI助手有什么可以帮您</view>
<view class="quick-list">
<view class="quick-item" wx:for="{{quickQuestions}}" wx:key="*this" data-question="{{item}}" bindtap="onQuickQuestionTap">
{{item}}
</view>
</view>
</view>
<view class="message-item {{item.type}}" wx:for="{{messages}}" wx:key="id" id="msg-{{item.id}}">
<view class="avatar {{item.type}}-avatar">
<text class="avatar-text">{{item.type === 'user' ? '我' : 'AI'}}</text>
</view>
<view class="message-content">
<view class="message-bubble {{item.type}}-bubble">
<t-chat-markdown
wx:if="{{item.contentType === 'text' && item.type === 'ai'}}"
class="message-markdown"
content="{{item.content}}"
/>
<text class="message-text" wx:elif="{{item.contentType === 'text'}}">{{item.content}}</text>
</view>
<text class="message-time" wx:if="{{item.time}}">{{item.time}}</text>
</view>
</view>
<view class="typing-indicator" wx:if="{{isTyping}}">
<view class="avatar ai-avatar">
<text class="avatar-text">AI</text>
</view>
<view class="typing-bubble">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</scroll-view>
<view class="chat-footer">
<view class="chat-toolbar" wx:if="{{messages.length > 0}}">
<view class="chat-toolbar-action" bindtap="clearChat">
<text>清空记录</text>
</view>
</view>
<view class="input-area">
<input
class="chat-input"
type="text"
placeholder="请输入您的问题..."
value="{{inputValue}}"
bindinput="onInputChange"
confirm-type="send"
bindconfirm="sendMessage"
adjust-position="{{true}}"
cursor-spacing="20"
hold-keyboard="{{true}}"
/>
<view class="send-btn {{canSend ? 'active' : ''}}" bindtap="sendMessage">
<text class="send-text">发送</text>
</view>
</view>
</view>
</view>

View File

@@ -1 +1,367 @@
/* pages/ai/index.wxss */
page {
background-color: #f5f6fa;
color: #1a1a2e;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f6fa;
}
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 24rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #e5e7eb;
flex-shrink: 0;
}
.chat-header-title {
font-size: 30rpx;
font-weight: 600;
color: #1a1a2e;
}
.message-list {
flex: 1;
padding: 20rpx;
padding-bottom: 200rpx;
box-sizing: border-box;
overflow: hidden;
}
.quick-questions {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 20rpx;
}
.welcome-text {
font-size: 32rpx;
color: #1a1a2e;
margin-bottom: 40rpx;
text-align: center;
font-weight: 500;
}
.quick-list {
display: flex;
flex-direction: column;
width: 100%;
gap: 20rpx;
}
.quick-item {
background-color: #ffffff;
border-radius: 16rpx;
padding: 24rpx 32rpx;
font-size: 28rpx;
color: #4c6ef5;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
text-align: center;
border: 1rpx solid #e5e7eb;
transition: all 0.2s ease;
}
.quick-item:active {
transform: scale(0.98);
background-color: rgba(76, 110, 245, 0.05);
}
.message-item {
display: flex;
flex-direction: column;
margin-bottom: 32rpx;
}
.message-item.ai {
align-items: flex-start;
}
.message-item.user {
align-items: flex-end;
}
.message-time {
font-size: 22rpx;
color: #9ca3af;
margin-top: 8rpx;
padding: 0 4rpx;
}
.message-item.ai .message-time {
text-align: left;
align-self: flex-start;
}
.message-item.user .message-time {
text-align: right;
align-self: flex-end;
}
.avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-bottom: 12rpx;
}
.user-avatar {
background: linear-gradient(135deg, #4c6ef5, #748ffc);
box-shadow: 0 4rpx 12rpx rgba(76, 110, 245, 0.25);
}
.ai-avatar {
background: linear-gradient(135deg, #20c997, #51cf66);
box-shadow: 0 4rpx 12rpx rgba(32, 201, 151, 0.25);
}
.avatar-text {
color: #fff;
font-size: 26rpx;
font-weight: 500;
}
.message-content {
display: flex;
flex-direction: column;
max-width: 95%;
}
.message-item.user .message-content {
align-items: flex-end;
}
.message-item.ai .message-content {
align-items: flex-start;
}
.message-bubble {
padding: 20rpx 24rpx;
border-radius: 16rpx;
font-size: 28rpx;
line-height: 1.6;
word-break: break-word;
max-width: 100%;
}
.user-bubble {
background: linear-gradient(135deg, #4c6ef5, #748ffc);
color: #fff;
border-bottom-right-radius: 4rpx;
box-shadow: 0 4rpx 12rpx rgba(76, 110, 245, 0.2);
margin-right: 4rpx;
}
.ai-bubble {
background-color: #ffffff;
color: #1a1a2e;
border-bottom-left-radius: 4rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
border: 1rpx solid #e5e7eb;
margin-left: 4rpx;
}
/* Markdown 渲染区域 */
.message-markdown {
width: 100%;
font-size: 28rpx;
color: #1a1a2e;
line-height: 1.6;
}
/* 表格样式覆盖(防止过宽) */
.message-markdown .t-chat-markdown-table {
width: 100%;
max-width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border: 1rpx solid #e5e7eb;
border-radius: 8rpx;
}
.message-markdown .t-chat-markdown-table__container {
display: table;
min-width: 100%;
max-width: max-content;
border-collapse: collapse;
white-space: normal;
table-layout: fixed;
word-break: break-word;
}
.message-markdown .t-chat-markdown-table__th,
.message-markdown .t-chat-markdown-table__td {
padding: 8rpx 12rpx;
font-size: 24rpx;
line-height: 1.4;
white-space: normal;
word-break: break-word;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 320rpx;
}
.message-markdown .t-chat-markdown-table__th {
background-color: #f5f6fa;
font-weight: 600;
color: #1a1a2e;
}
.message-markdown .t-chat-markdown-table__tr:nth-child(2n) {
background-color: #fafbfc;
}
/* 代码块样式覆盖 */
.message-markdown .t-chat-markdown-codespan,
.message-markdown .t-chat-markdown-code {
font-size: 24rpx;
word-break: break-all;
}
.typing-indicator {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 32rpx;
}
.typing-bubble {
display: flex;
align-items: center;
gap: 12rpx;
background-color: #ffffff;
padding: 24rpx 32rpx;
border-radius: 16rpx;
border-bottom-left-radius: 4rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
border: 1rpx solid #e5e7eb;
}
.dot {
width: 16rpx;
height: 16rpx;
background-color: #9ca3af;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0.6);
}
40% {
transform: scale(1);
}
}
.input-area {
display: flex;
align-items: center;
padding: 20rpx 24rpx;
background-color: #ffffff;
gap: 16rpx;
box-sizing: border-box;
}
.chat-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
background-color: #ffffff;
}
.chat-toolbar {
display: flex;
align-items: right;
justify-content: right;
padding: 12rpx 24rpx;
background-color: #f5f6fa;
border-top: 1rpx solid #e5e7eb;
border-bottom: 1rpx solid #e5e7eb;
}
.chat-toolbar-action {
display: flex;
align-items: right;
justify-content: right;
padding: 6rpx 24rpx;
border-radius: 24rpx;
background-color: #ffffff;
border: 1rpx solid #e5e7eb;
transition: all 0.2s ease;
}
.chat-toolbar-action:active {
transform: scale(0.96);
background-color: rgba(76, 110, 245, 0.08);
border-color: #4c6ef5;
}
.chat-toolbar-action text {
font-size: 24rpx;
color: #6b7280;
}
.chat-input {
flex: 1;
height: 72rpx;
background-color: #f5f6fa;
border-radius: 36rpx;
padding: 0 28rpx;
font-size: 28rpx;
color: #1a1a2e;
border: 1rpx solid #e5e7eb;
}
.chat-input::placeholder {
color: #9ca3af;
}
.send-btn {
width: 120rpx;
height: 72rpx;
background-color: #e5e7eb;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.send-btn.active {
background: linear-gradient(135deg, #4c6ef5, #748ffc);
box-shadow: 0 4rpx 12rpx rgba(76, 110, 245, 0.25);
}
.send-text {
color: #9ca3af;
font-size: 28rpx;
font-weight: 500;
transition: color 0.2s ease;
}
.send-btn.active .send-text {
color: #FFFFFF;
}

View File

@@ -1,66 +1,269 @@
// pages/customer/index.js
const API = require('../../utils/api.js')
// 性别映射
const SEX_MAP = {
male: { label: '男', text: '先生' },
female: { label: '女', text: '女士' },
other: { label: '其他', text: '' },
'': { label: '未知', text: '' }
}
Page({
/**
* 页面的初始数据
*/
data: {
// 搜索相关
searchKey: '',
// 客户列表
customerList: [],
// 分页
page: 1,
pageSize: 10,
hasMore: true,
// 加载状态
isLoading: false,
isLoadingMore: false,
isRefreshing: false,
// 统计数据
stats: {
total: 0,
male: 0,
female: 0
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadCustomerList();
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 页面显示时如果已有数据则刷新
if (this.data.customerList.length > 0) {
this.refreshData();
}
},
/**
* 生命周期函数--监听页面隐藏
* 格式化日期
*/
onHide() {
formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
/**
* 生命周期函数--监听页面卸载
* 处理客户数据
*/
onUnload() {
processCustomers(customers) {
return customers.map(customer => {
const profile = customer.profile || {};
const sexInfo = SEX_MAP[profile.sex] || SEX_MAP[''];
return {
...customer,
name: profile.name || '未知姓名',
mobile: profile.mobile || '暂无电话',
sex: profile.sex || '',
sexLabel: sexInfo.label,
sexText: sexInfo.text,
avatar: profile.avatar || '',
avatarText: profile.name ? profile.name[0].toUpperCase() : '客',
birth: profile.birth ? this.formatDate(profile.birth) : '',
createdAt: customer.meta?.createtime ? this.formatDate(customer.meta.createtime) : '',
locationText: this.getLocationText(customer)
};
});
},
/**
* 页面相关事件处理函数--监听用户下拉动作
* 获取位置文本
*/
getLocationText(customer) {
const location = customer.location || {};
if (location.province || location.city) {
return `${location.province || ''}${location.city || ''}`;
}
return '';
},
/**
* 计算统计数据
*/
calculateStats(customers) {
const stats = {
total: customers.length,
male: 0,
female: 0
};
customers.forEach(customer => {
const sex = customer.profile?.sex || '';
if (sex === 'male') stats.male++;
else if (sex === 'female') stats.female++;
});
return stats;
},
/**
* 加载客户列表
*/
loadCustomerList(isRefresh = false) {
if (this.data.isLoading || this.data.isLoadingMore) return;
const { page, pageSize, searchKey } = this.data;
this.setData({
isLoading: !isRefresh && page === 1,
isLoadingMore: isRefresh && page > 1
});
const params = {
page,
pageSize
};
if (searchKey && searchKey.trim()) {
params.keyword = searchKey.trim();
}
API.user.userList(params)
.then(res => {
if (res.code !== 0) {
wx.showToast({ title: res.message || '获取客户列表失败', icon: 'none' });
this.setData({ isLoading: false, isLoadingMore: false, isRefreshing: false });
return;
}
const data = res.data || {};
const list = data.users || [];
const total = data?.users?.length || 0;
const processedCustomers = this.processCustomers(list);
let allCustomers = isRefresh && page > 1
? [...this.data.customerList, ...processedCustomers]
: processedCustomers;
// 如果有搜索条件,在前端过滤
if (searchKey && searchKey.trim()) {
const keyword = searchKey.trim().toLowerCase();
allCustomers = allCustomers.filter(customer => {
return customer.name.toLowerCase().includes(keyword) ||
customer.mobile.includes(keyword);
});
}
const stats = this.calculateStats(allCustomers);
this.setData({
customerList: allCustomers,
stats,
isLoading: false,
isLoadingMore: false,
isRefreshing: false,
hasMore: allCustomers.length < total
});
})
.catch(err => {
console.error('获取客户列表失败', err);
wx.showToast({ title: '网络错误,请重试', icon: 'none' });
this.setData({ isLoading: false, isLoadingMore: false, isRefreshing: false });
});
},
/**
* 刷新数据
*/
refreshData() {
this.setData({ page: 1, hasMore: true }, () => {
this.loadCustomerList();
});
},
/**
* 搜索输入
*/
onSearchInput(e) {
this.setData({ searchKey: e.detail.value });
},
/**
* 搜索确认
*/
onSearch() {
this.setData({ page: 1, hasMore: true, customerList: [] }, () => {
this.loadCustomerList();
});
},
/**
* 清除搜索
*/
onClearSearch() {
if (this.data.searchKey) {
this.setData({ searchKey: '', page: 1, hasMore: true, customerList: [] }, () => {
this.loadCustomerList();
});
}
},
/**
* 客户详情
*/
onCustomerDetail(e) {
const { id } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/customer/detail?id=${id}`
});
},
/**
* 拨打电话
*/
onCallPhone(e) {
const { phone } = e.currentTarget.dataset;
if (!phone) {
wx.showToast({ title: '暂无电话号码', icon: 'none' });
return;
}
wx.makePhoneCall({
phoneNumber: phone,
fail: () => {
wx.showToast({ title: '拨打电话失败', icon: 'none' });
}
});
},
/**
* 下拉刷新
*/
onPullDownRefresh() {
this.setData({ isRefreshing: true, page: 1, hasMore: true }, () => {
this.loadCustomerList();
});
wx.stopPullDownRefresh();
},
/**
* 页面上拉触底事件的处理函数
* 上拉加载更多
*/
onReachBottom() {
if (!this.data.hasMore || this.data.isLoadingMore) return;
this.setData({ page: this.data.page + 1 }, () => {
this.loadCustomerList(true);
});
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
return {
title: '客户管理',
path: '/pages/customer/index'
};
}
})
});

View File

@@ -1,3 +1,11 @@
{
"usingComponents": {}
}
"navigationBarTitleText": "客户管理",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#ffffff",
"usingComponents": {
"t-empty": "tdesign-miniprogram/empty/empty",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

View File

@@ -1 +1,328 @@
/* pages/customer/index.wxss */
/* pages/customer/index.less */
// 颜色变量
@bg-primary: #f5f6fa;
@bg-secondary: #ffffff;
@bg-card: #ffffff;
@accent-primary: #4c6ef5;
@accent-secondary: #6b7aff;
@accent-gradient-start: #4c6ef5;
@accent-gradient-end: #748ffc;
@text-primary: #1a1a2e;
@text-secondary: #6b7280;
@text-muted: #9ca3af;
@border-color: #e5e7eb;
@male-color: #4c6ef5;
@female-color: #ff6b6b;
@divider-color: #f3f4f6;
page {
background-color: @bg-primary;
color: @text-primary;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.customer-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: @bg-primary;
}
// ========== 搜索区域 ==========
.search-section {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
background-color: @bg-secondary;
border-bottom: 1rpx solid @border-color;
}
.search-bar {
flex: 1;
display: flex;
align-items: center;
gap: 12rpx;
padding: 16rpx 24rpx;
background-color: @bg-primary;
border-radius: 32rpx;
border: 1rpx solid @border-color;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: @text-primary;
height: 40rpx;
line-height: 40rpx;
&::placeholder {
color: @text-muted;
}
}
.search-clear {
display: flex;
align-items: center;
justify-content: center;
padding: 4rpx;
}
.search-btn {
padding: 16rpx 28rpx;
background: linear-gradient(135deg, @accent-gradient-start, @accent-gradient-end);
color: #ffffff;
font-size: 26rpx;
font-weight: 500;
border-radius: 32rpx;
white-space: nowrap;
box-shadow: 0 4rpx 16rpx rgba(76, 110, 245, 0.25);
&:active {
opacity: 0.9;
transform: scale(0.98);
}
}
// ========== 统计区域 ==========
.stats-section {
padding: 20rpx 24rpx 0;
}
.stats-card {
display: flex;
align-items: center;
justify-content: space-around;
padding: 28rpx 24rpx;
background-color: @bg-card;
border-radius: 24rpx;
border: 1rpx solid @border-color;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.stats-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.stats-value {
font-size: 40rpx;
font-weight: 700;
color: @text-primary;
&.male {
color: @male-color;
}
&.female {
color: @female-color;
}
}
.stats-label {
font-size: 24rpx;
color: @text-secondary;
}
.stats-divider {
width: 1rpx;
height: 60rpx;
background-color: @divider-color;
}
// ========== 客户列表区域 ==========
.customer-list {
flex: 1;
overflow: hidden;
padding: 20rpx 24rpx 0;
}
.customer-scroll {
height: 100%;
}
.loading-container,
.empty-container {
display: flex;
align-items: center;
justify-content: center;
padding: 200rpx 40rpx;
}
.loading-text {
color: @text-secondary !important;
font-size: 26rpx !important;
}
.empty-text {
color: @text-secondary !important;
font-size: 28rpx !important;
}
.empty-action {
margin-top: 30rpx;
padding: 16rpx 48rpx;
background: linear-gradient(135deg, @accent-gradient-start, @accent-gradient-end);
color: #ffffff;
font-size: 28rpx;
border-radius: 32rpx;
display: inline-block;
}
// ========== 客户卡片 ==========
.customer-cards {
padding-bottom: 40rpx;
}
.customer-card {
display: flex;
align-items: center;
gap: 24rpx;
margin-bottom: 20rpx;
padding: 28rpx;
background-color: @bg-card;
border-radius: 24rpx;
border: 1rpx solid @border-color;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
}
// 头像
.customer-avatar {
flex-shrink: 0;
}
.avatar-img {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
object-fit: cover;
}
.avatar-placeholder {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, @accent-gradient-start, @accent-gradient-end);
box-shadow: 0 4rpx 12rpx rgba(76, 110, 245, 0.25);
&.male {
background: linear-gradient(135deg, #4c6ef5, #748ffc);
}
&.female {
background: linear-gradient(135deg, #ff6b6b, #ff8787);
}
}
.avatar-text {
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
}
// 客户信息
.customer-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
min-width: 0;
}
.info-row {
display: flex;
align-items: center;
gap: 16rpx;
flex-wrap: wrap;
}
.customer-name {
font-size: 32rpx;
font-weight: 600;
color: @text-primary;
}
.gender-tag {
display: inline-flex;
align-items: center;
padding: 4rpx 14rpx;
border-radius: 10rpx;
font-size: 22rpx;
font-weight: 500;
&.male {
background-color: rgba(76, 110, 245, 0.1);
color: @male-color;
}
&.female {
background-color: rgba(255, 107, 107, 0.1);
color: @female-color;
}
}
.customer-date {
font-size: 22rpx;
color: @text-muted;
margin-left: auto;
}
.customer-phone {
font-size: 26rpx;
color: @text-secondary;
}
.customer-location {
font-size: 24rpx;
color: @text-muted;
}
// 操作按钮
.customer-action {
flex-shrink: 0;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background-color: rgba(76, 110, 245, 0.08);
transition: all 0.2s ease;
&:active {
background-color: rgba(76, 110, 245, 0.15);
transform: scale(0.95);
}
&.call {
background-color: rgba(76, 110, 245, 0.08);
}
}
// ========== 加载更多 ==========
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 20rpx;
}
.no-more {
font-size: 24rpx;
color: @text-muted;
}

View File

@@ -1,2 +1,116 @@
<!--pages/customer/index.wxml-->
<text>pages/customer/index.wxml</text>
<view class="customer-page">
<!-- 搜索区域 -->
<view class="search-section">
<view class="search-bar">
<t-icon name="search" size="32rpx" color="#9ca3af" />
<input
class="search-input"
type="text"
placeholder="搜索客户姓名或手机号"
value="{{searchKey}}"
bindinput="onSearchInput"
confirm-type="search"
bindconfirm="onSearch"
/>
<view class="search-clear" wx:if="{{searchKey}}" bindtap="onClearSearch">
<t-icon name="close-circle" size="32rpx" color="#9ca3af" />
</view>
</view>
<view class="search-btn" bindtap="onSearch">搜索</view>
</view>
<!-- 统计卡片 -->
<view class="stats-section" wx:if="{{!isLoading && customerList.length > 0}}">
<view class="stats-card">
<view class="stats-item">
<text class="stats-value">{{stats.total}}</text>
<text class="stats-label">客户总数</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="stats-value male">{{stats.male}}</text>
<text class="stats-label">男</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item">
<text class="stats-value female">{{stats.female}}</text>
<text class="stats-label">女</text>
</view>
</view>
</view>
<!-- 客户列表 -->
<view class="customer-list">
<scroll-view
scroll-y
class="customer-scroll"
refresher-enabled
refresher-triggered="{{isRefreshing}}"
bindrefresherrefresh="onPullDownRefresh"
bindscrolltolower="onReachBottom"
>
<!-- 加载中 -->
<view class="loading-container" wx:if="{{isLoading && customerList.length === 0}}">
<t-loading theme="spinner" size="40rpx" text="加载中..." t-class-text="loading-text" />
</view>
<!-- 空状态 -->
<view class="empty-container" wx:elif="{{!isLoading && customerList.length === 0}}">
<t-empty icon="user" description="暂无客户数据" t-class-description="empty-text">
<view slot="action">
<view class="empty-action" bindtap="refreshData">刷新试试</view>
</view>
</t-empty>
</view>
<!-- 客户卡片列表 -->
<view class="customer-cards" wx:else>
<view
class="customer-card"
wx:for="{{customerList}}"
wx:key="_id"
data-id="{{item._id}}"
bindtap="onCustomerDetail"
>
<!-- 头像 -->
<view class="customer-avatar">
<image wx:if="{{item.avatar}}" class="avatar-img" src="{{item.avatar}}" mode="aspectFill" />
<view wx:else class="avatar-placeholder {{item.sex}}">
<text class="avatar-text">{{item.avatarText}}</text>
</view>
</view>
<!-- 客户信息 -->
<view class="customer-info">
<view class="info-row">
<text class="customer-name">{{item.name}}</text>
<view class="gender-tag {{item.sex}}" wx:if="{{item.sex}}">
<text>{{item.sexLabel}}</text>
</view>
<text class="customer-date" wx:if="{{item.createdAt}}">{{item.createdAt}}</text>
</view>
<view class="info-row">
<text class="customer-phone">{{item.mobile}}</text>
<text class="customer-location" wx:if="{{item.locationText}}">{{item.locationText}}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="customer-action">
<view class="action-btn call" data-phone="{{item.mobile}}" catchtap="onCallPhone">
<t-icon name="call" size="32rpx" color="#4c6ef5" />
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more">
<t-loading wx:if="{{isLoadingMore}}" theme="spinner" size="32rpx" text="加载中..." t-class-text="loading-text" />
<text class="no-more" wx:elif="{{!hasMore}}">没有更多了</text>
</view>
</view>
</scroll-view>
</view>
</view>

View File

@@ -1,66 +1,90 @@
// pages/home/index.js
const API = require('../../utils/api.js')
Page({
/**
* 页面的初始数据
*/
data: {
todayCount: 0,
pendingCount: 0,
completedCount: 0,
menuList: [
{ icon: '/images/icon_order.png', name: '订单管理', url: '/pages/order/index' },
{ icon: '/images/icon_patient.png', name: '患者管理', url: '/pages/patient/index' },
{ icon: '/images/icon_escort.png', name: '陪诊员管理', url: '/pages/escort/index' },
{ icon: '/images/icon_schedule.png', name: '排班管理', url: '/pages/schedule/index' },
{ icon: '/images/icon_stats.png', name: '数据统计', url: '/pages/stats/index' },
{ icon: '/images/icon_setting.png', name: '系统设置', url: '/pages/setting/index' }
],
todayOrders: [],
statusMap: {
pending: '待确认',
confirmed: '已确认',
in_progress: '进行中',
completed: '已完成',
cancelled: '已取消'
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
return {
title: '暖橙陪诊后台', // 转发标题
path: '/pages/home/index',
}
},
onShareTimeline: function () {
return {
title: '暖橙陪诊后台',
}
},
onLoad(options) {
this.getTodayOrders();
this.getStats();
},
onShow() {
this.getTodayOrders();
this.getStats();
},
async getStats() {
const today = new Date().toISOString().substring(0, 10);
const [todayRes, pendingRes, completedRes] = await Promise.all([
API.escort.getMyRecords({ appointmentDate: today }),
API.escort.getMyRecords({ status: ['pending', 'confirmed'] }),
API.escort.getMyRecords({ status: ['completed'] })
]);
this.setData({
todayCount: todayRes.code === 0 ? (todayRes.data.records || []).length : 0,
pendingCount: pendingRes.code === 0 ? (pendingRes.data.records || []).length : 0,
completedCount: completedRes.code === 0 ? (completedRes.data.records || []).length : 0,
});
},
async getTodayOrders() {
const res = await API.escort.getMyRecords({
appointmentDate: new Date().toISOString().substring(0, 10),
});
if (res.code == 0) {
const records = (res.data.records || []).map(item => {
if (item.schedule && item.schedule.date) {
const d = new Date(item.schedule.date);
item.schedule.date = d.toISOString().substring(0, 10) + ' ' + d.toTimeString().substring(0, 5);
}
return item;
});
this.setData({ todayOrders: records });
}
},
navigateTo(e) {
const url = e.currentTarget.dataset.url
wx.navigateTo({ url })
},
viewAllOrders() {
wx.navigateTo({
url: '/pages/order/index'
})
}
})
})

View File

@@ -1,2 +1,69 @@
<!--pages/home/index.wxml-->
<text>pages/home/index.wxml</text>
<view class="container">
<!-- 顶部统计卡片 -->
<view class="stats-section">
<view class="stats-card">
<text class="stats-num">{{todayCount}}</text>
<text class="stats-label">今日订单</text>
</view>
<view class="stats-card">
<text class="stats-num">{{pendingCount}}</text>
<text class="stats-label">待处理</text>
</view>
<view class="stats-card">
<text class="stats-num">{{completedCount}}</text>
<text class="stats-label">已完成</text>
</view>
</view>
<!-- 功能菜单 -->
<view class="menu-section">
<view class="section-title">功能菜单</view>
<view class="menu-grid">
<view class="menu-item" wx:for="{{menuList}}" wx:key="index" bindtap="navigateTo" data-url="{{item.url}}">
<view class="menu-icon">
<text class="icon-text">{{item.name[0]}}</text>
</view>
<text class="menu-name">{{item.name}}</text>
</view>
</view>
</view>
<!-- 今日订单 -->
<view class="order-section">
<view class="section-header">
<text class="section-title">今日订单</text>
<text class="view-all" bindtap="viewAllOrders">查看全部 ></text>
</view>
<view class="order-list">
<view class="order-item" wx:for="{{todayOrders}}" wx:key="_id">
<view class="order-header">
<text class="order-id">{{item._id}}</text>
<text class="order-status status-{{item.status}}">{{statusMap[item.status] || item.status}}</text>
</view>
<view class="order-info">
<view class="info-row">
<text class="info-label">患者</text>
<text class="info-value">{{item.patient.name}}</text>
</view>
<view class="info-row">
<text class="info-label">医院</text>
<text class="info-value">{{item.hospital.name}} · {{item.hospital.department}}</text>
</view>
<view class="info-row">
<text class="info-label">时间</text>
<text class="info-value">{{item.schedule.date}}</text>
</view>
<view class="info-row">
<text class="info-label">服务</text>
<text class="info-value">{{item.escort.serviceName}}</text>
</view>
<view class="info-row">
<text class="info-label">费用</text>
<text class="info-value fee-value">¥{{item.payment.totalFee}}</text>
</view>
</view>
</view>
</view>
</view>
</view>

View File

@@ -1 +1,208 @@
/* pages/home/index.wxss */
/* pages/home/index.wxss */
.container {
padding: 20rpx;
background-color: #f5f6fa;
min-height: 100vh;
}
/* 统计卡片 */
.stats-section {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
}
.stats-card {
flex: 1;
background: #fff;
border-radius: 16rpx;
padding: 30rpx 0;
margin: 0 10rpx;
text-align: center;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
}
.stats-card:first-child {
margin-left: 0;
}
.stats-card:last-child {
margin-right: 0;
}
.stats-num {
display: block;
font-size: 48rpx;
font-weight: 600;
color: #2c3e50;
margin-bottom: 8rpx;
}
.stats-label {
display: block;
font-size: 26rpx;
color: #7f8c8d;
}
/* 功能菜单 */
.menu-section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #2c3e50;
margin-bottom: 24rpx;
}
.menu-grid {
display: flex;
flex-wrap: wrap;
}
.menu-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 0;
}
.menu-icon {
width: 88rpx;
height: 88rpx;
background: #e8f4fd;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
}
.icon-text {
font-size: 36rpx;
color: #3498db;
font-weight: 600;
}
.menu-name {
font-size: 26rpx;
color: #555;
}
/* 今日订单 */
.order-section {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.section-header .section-title {
margin-bottom: 0;
}
.view-all {
font-size: 26rpx;
color: #3498db;
}
.order-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.order-item {
background: #f8f9fa;
border-radius: 12rpx;
padding: 24rpx;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.order-id {
font-size: 26rpx;
color: #7f8c8d;
}
.order-status {
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx;
}
.status-pending {
background: #fff3e0;
color: #f39c12;
}
.status-in_progress {
background: #e8f5e9;
color: #27ae60;
}
.status-confirmed {
background: #e3f2fd;
color: #2980b9;
}
.status-pending {
background: #fff3e0;
color: #f39c12;
}
.status-completed {
background: #f3e5f5;
color: #8e44ad;
}
.status-cancelled {
background: #fafafa;
color: #95a5a6;
}
.order-info {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.info-row {
display: flex;
align-items: center;
}
.info-label {
font-size: 26rpx;
color: #95a5a6;
width: 80rpx;
flex-shrink: 0;
}
.info-value {
font-size: 28rpx;
color: #2c3e50;
}
.fee-value {
color: #e74c3c;
font-weight: 600;
}

View File

@@ -1,4 +1,5 @@
// pages/order/index.js
const API = require('../../utils/api.js')
// 状态映射配置
const STATUS_MAP = {
@@ -19,322 +20,6 @@ const STATUS_FILTERS = [
{ label: '已取消', value: 'cancelled', count: 0 }
];
// Mock 数据 - 基于 escort_record.js 的数据结构
const MOCK_ORDERS = [
{
_id: 'ORD20240530001',
orderNo: '20240530001',
userId: 'user001',
patient: {
name: '张三',
mobile: '138****1234',
sex: 'male',
age: 65,
idnumber: '310***********1234'
},
escort: {
serviceId: 1,
serviceName: '全程陪诊服务'
},
hospital: {
province: '上海',
name: '复旦大学附属中山医院',
address: '上海市徐汇区枫林路180号',
department: '心内科',
doctor: '李医生',
medicalRecordNo: 'MR202405001'
},
schedule: {
date: '2024-06-01T09:00:00.000Z',
dateText: '06月01日',
startTime: '09:00',
endTime: '12:00',
duration: 180
},
attendant: {
id: 'att001',
name: '王陪诊',
sex: 'female'
},
payment: {
totalFee: 298,
paidFee: 298,
status: 'paid'
},
notes: {
patientNote: '患者行动不便,需要轮椅',
escortNote: '',
medicalSummary: ''
},
status: 'pending',
statusText: '待确认',
meta: {
createtime: '2024-05-30T10:00:00.000Z',
updatetime: '2024-05-30T10:00:00.000Z'
}
},
{
_id: 'ORD20240529002',
orderNo: '20240529002',
userId: 'user002',
patient: {
name: '李四',
mobile: '139****5678',
sex: 'female',
age: 42,
idnumber: '310***********5678'
},
escort: {
serviceId: 2,
serviceName: '挂号陪诊服务'
},
hospital: {
province: '上海',
name: '上海交通大学医学院附属瑞金医院',
address: '上海市黄浦区瑞金二路197号',
department: '内分泌科',
doctor: '张医生',
medicalRecordNo: 'MR202405002'
},
schedule: {
date: '2024-05-31T14:00:00.000Z',
dateText: '05月31日',
startTime: '14:00',
endTime: '16:00',
duration: 120
},
attendant: {
id: 'att002',
name: '赵陪诊',
sex: 'male'
},
payment: {
totalFee: 198,
paidFee: 198,
status: 'paid'
},
notes: {
patientNote: '需要帮忙取药',
escortNote: '',
medicalSummary: ''
},
status: 'confirmed',
statusText: '已确认',
meta: {
createtime: '2024-05-29T08:30:00.000Z',
updatetime: '2024-05-29T15:00:00.000Z'
}
},
{
_id: 'ORD20240528003',
orderNo: '20240528003',
userId: 'user003',
patient: {
name: '王五',
mobile: '137****9012',
sex: 'male',
age: 78,
idnumber: '310***********9012'
},
escort: {
serviceId: 1,
serviceName: '全程陪诊服务'
},
hospital: {
province: '上海',
name: '上海市第六人民医院',
address: '上海市徐汇区宜山路600号',
department: '骨科',
doctor: '刘医生',
medicalRecordNo: 'MR202405003'
},
schedule: {
date: '2024-05-30T08:30:00.000Z',
dateText: '05月30日',
startTime: '08:30',
endTime: '11:30',
duration: 180
},
attendant: {
id: 'att003',
name: '陈陪诊',
sex: 'female'
},
payment: {
totalFee: 398,
paidFee: 200,
status: 'partial'
},
notes: {
patientNote: '听力不好,请大声说话',
escortNote: '已接到患者,正在前往医院',
medicalSummary: ''
},
status: 'in_progress',
statusText: '进行中',
meta: {
createtime: '2024-05-28T16:00:00.000Z',
updatetime: '2024-05-30T08:35:00.000Z'
}
},
{
_id: 'ORD20240525004',
orderNo: '20240525004',
userId: 'user004',
patient: {
name: '赵六',
mobile: '136****3456',
sex: 'female',
age: 55,
idnumber: '310***********3456'
},
escort: {
serviceId: 3,
serviceName: '检查陪诊服务'
},
hospital: {
province: '上海',
name: '华东医院',
address: '上海市静安区延安西路221号',
department: '体检中心',
doctor: '',
medicalRecordNo: 'MR202405004'
},
schedule: {
date: '2024-05-28T07:30:00.000Z',
dateText: '05月28日',
startTime: '07:30',
endTime: '11:00',
duration: 210
},
attendant: {
id: 'att004',
name: '孙陪诊',
sex: 'male'
},
payment: {
totalFee: 258,
paidFee: 258,
status: 'paid'
},
notes: {
patientNote: '需要空腹检查',
escortNote: '服务完成,患者已安全送回家',
medicalSummary: '完成全身检查,各项指标正常'
},
status: 'completed',
statusText: '已完成',
meta: {
createtime: '2024-05-25T09:00:00.000Z',
updatetime: '2024-05-28T11:30:00.000Z'
}
},
{
_id: 'ORD20240524005',
orderNo: '20240524005',
userId: 'user005',
patient: {
name: '钱七',
mobile: '135****7890',
sex: 'male',
age: 33,
idnumber: '310***********7890'
},
escort: {
serviceId: 2,
serviceName: '挂号陪诊服务'
},
hospital: {
province: '上海',
name: '上海市第一人民医院',
address: '上海市虹口区武进路85号',
department: '眼科',
doctor: '周医生',
medicalRecordNo: 'MR202405005'
},
schedule: {
date: '2024-05-27T10:00:00.000Z',
dateText: '05月27日',
startTime: '10:00',
endTime: '11:00',
duration: 60
},
attendant: {
id: '',
name: '',
sex: 'none'
},
payment: {
totalFee: 128,
paidFee: 0,
status: 'unpaid'
},
notes: {
patientNote: '临时有事,需要取消',
escortNote: '',
medicalSummary: ''
},
status: 'cancelled',
statusText: '已取消',
meta: {
createtime: '2024-05-24T14:00:00.000Z',
updatetime: '2024-05-26T09:00:00.000Z'
}
},
{
_id: 'ORD20240523006',
orderNo: '20240523006',
userId: 'user006',
patient: {
name: '孙八',
mobile: '134****2468',
sex: 'female',
age: 60,
idnumber: '310***********2468'
},
escort: {
serviceId: 1,
serviceName: '全程陪诊服务'
},
hospital: {
province: '上海',
name: '上海中医药大学附属龙华医院',
address: '上海市徐汇区宛平南路725号',
department: '中医内科',
doctor: '吴医生',
medicalRecordNo: 'MR202405006'
},
schedule: {
date: '2024-06-02T08:00:00.000Z',
dateText: '06月02日',
startTime: '08:00',
endTime: '11:00',
duration: 180
},
attendant: {
id: 'att005',
name: '周陪诊',
sex: 'female'
},
payment: {
totalFee: 328,
paidFee: 328,
status: 'paid'
},
notes: {
patientNote: ' prefer 女陪诊员',
escortNote: '',
medicalSummary: ''
},
status: 'pending',
statusText: '待确认',
meta: {
createtime: '2024-05-23T11:00:00.000Z',
updatetime: '2024-05-23T11:00:00.000Z'
}
}
];
Page({
/**
@@ -376,7 +61,6 @@ Page({
* 生命周期函数--监听页面显示
*/
onShow() {
// 页面显示时刷新数据
if (this.data.orderList.length > 0) {
this.refreshData();
}
@@ -468,7 +152,10 @@ Page({
return orders.map(order => ({
...order,
statusText: STATUS_MAP[order.status]?.text || order.status,
'schedule.dateText': this.formatDate(order.schedule.date)
schedule: {
...order.schedule,
dateText: this.formatDate(order.schedule?.date)
}
}));
},
@@ -482,40 +169,53 @@ Page({
this.setData({ isLoading: !isRefresh, isLoadingMore: isRefresh && page > 1 });
// 模拟 API 请求延迟
setTimeout(() => {
// 筛选数据
let filteredOrders = MOCK_ORDERS;
if (currentStatus) {
filteredOrders = MOCK_ORDERS.filter(order => order.status === currentStatus);
}
const params = {
page,
pageSize,
status: 'pending,confirmed,in_progress,completed,cancelled'
};
// 分页
const start = (page - 1) * pageSize;
const end = start + pageSize;
const pageData = filteredOrders.slice(start, end);
API.escort.getMyRecords(params)
.then(res => {
if (res.code !== 0) {
wx.showToast({ title: res.message || '获取订单失败', icon: 'none' });
this.setData({ isLoading: false, isLoadingMore: false, isRefreshing: false });
return;
}
// 处理数据
const processedOrders = this.processOrders(pageData);
const data = res.data || {};
const list = data.records || [];
let total = list.length || 0;
// 计算统计
const stats = this.calculateStats(MOCK_ORDERS);
this.updateFilterCounts(stats);
const processedOrders = this.processOrders(list);
// 更新列表
const orderList = isRefresh && page > 1
? [...this.data.orderList, ...processedOrders]
: processedOrders;
const allOrders = isRefresh && page > 1
? [...this.data.orderList, ...processedOrders]
: processedOrders;
this.setData({
orderList,
stats,
isLoading: false,
isLoadingMore: false,
isRefreshing: false,
hasMore: end < filteredOrders.length
const stats = this.calculateStats(allOrders);
this.updateFilterCounts(stats);
const { currentStatus } = this.data;
const orderList = currentStatus
? allOrders.filter(order => order.status === currentStatus)
: allOrders;
total = orderList.length;
this.setData({
orderList,
stats,
isLoading: false,
isLoadingMore: false,
isRefreshing: false,
hasMore: orderList.length < total
});
})
.catch(err => {
console.error('获取订单列表失败', err);
wx.showToast({ title: '网络错误,请重试', icon: 'none' });
this.setData({ isLoading: false, isLoadingMore: false, isRefreshing: false });
});
}, 600);
},
/**
@@ -562,9 +262,6 @@ Page({
const order = this.data.orderList.find(item => item._id === id);
if (!order) return;
// 阻止冒泡,防止触发卡片点击
e.stopPropagation();
const actionMap = {
confirm: { title: '确认订单', content: '确认接受此订单?', nextStatus: 'confirmed' },
cancel: { title: '取消订单', content: '确定要取消此订单?', nextStatus: 'cancelled' },
@@ -589,7 +286,6 @@ Page({
confirmColor: '#4c6ef5',
success: (res) => {
if (res.confirm) {
// 更新订单状态
this.updateOrderStatus(id, actionConfig.nextStatus);
}
}
@@ -602,34 +298,38 @@ Page({
updateOrderStatus(id, newStatus) {
wx.showLoading({ title: '处理中...' });
// 模拟 API 请求
setTimeout(() => {
const orderList = this.data.orderList.map(order => {
if (order._id === id) {
return {
...order,
status: newStatus,
statusText: STATUS_MAP[newStatus]?.text || newStatus,
'meta.updatetime': new Date().toISOString()
};
API.escort.updateStatus(id, { status: newStatus })
.then(res => {
if (res.code !== 0) {
wx.showToast({ title: res.message || '操作失败', icon: 'none' });
wx.hideLoading();
return;
}
return order;
});
// 重新计算统计
const allOrders = MOCK_ORDERS.map(order => {
if (order._id === id) {
return { ...order, status: newStatus };
}
return order;
});
const stats = this.calculateStats(allOrders);
this.updateFilterCounts(stats);
const orderList = this.data.orderList.map(order => {
if (order._id === id) {
return {
...order,
status: newStatus,
statusText: STATUS_MAP[newStatus]?.text || newStatus,
'meta.updatetime': new Date().toISOString()
};
}
return order;
});
this.setData({ orderList, stats });
wx.hideLoading();
wx.showToast({ title: '操作成功', icon: 'success' });
}, 500);
const stats = this.calculateStats(orderList);
this.updateFilterCounts(stats);
this.setData({ orderList, stats });
wx.hideLoading();
wx.showToast({ title: '操作成功', icon: 'success' });
})
.catch(err => {
console.error('更新订单状态失败', err);
wx.showToast({ title: '网络错误,请重试', icon: 'none' });
wx.hideLoading();
});
},
/**

View File

@@ -1,8 +1,8 @@
{
"navigationBarTitleText": "订单管理",
"navigationBarBackgroundColor": "#1a1f3c",
"navigationBarTextStyle": "white",
"backgroundColor": "#0f1535",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#ffffff",
"usingComponents": {
"t-tabs": "tdesign-miniprogram/tabs/tabs",
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",

View File

@@ -1,24 +1,23 @@
/* pages/order/index.less */
// 颜色变量
@bg-primary: #0f1535;
@bg-secondary: #1a1f3c;
@bg-card: #1e2548;
@bg-card-hover: #252d5a;
// 颜色变量 - 参考图浅色社交风格
@bg-primary: #f5f6fa;
@bg-secondary: #ffffff;
@bg-card: #ffffff;
@accent-primary: #4c6ef5;
@accent-secondary: #6b7aff;
@accent-gradient-start: #4c6ef5;
@accent-gradient-end: #748ffc;
@text-primary: #ffffff;
@text-secondary: #a0a8d0;
@text-muted: #6b7298;
@border-color: #2a3366;
@text-primary: #1a1a2e;
@text-secondary: #6b7280;
@text-muted: #9ca3af;
@border-color: #e5e7eb;
@status-pending: #f59f00;
@status-confirmed: #4c6ef5;
@status-in-progress: #20c997;
@status-completed: #51cf66;
@status-cancelled: #ff6b6b;
@divider-color: #2a3366;
@divider-color: #f3f4f6;
page {
background-color: @bg-primary;
@@ -33,79 +32,11 @@ page {
background-color: @bg-primary;
}
// ========== 顶部统计区域 ==========
.header-section {
position: relative;
padding: 30rpx 30rpx 40rpx;
overflow: hidden;
.header-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, @accent-gradient-start 0%, @accent-gradient-end 100%);
border-radius: 0 0 40rpx 40rpx;
opacity: 0.15;
}
&::before {
content: '';
position: absolute;
top: -100rpx;
right: -100rpx;
width: 300rpx;
height: 300rpx;
background: radial-gradient(circle, rgba(76, 110, 245, 0.2) 0%, transparent 70%);
border-radius: 50%;
}
}
.stats-container {
position: relative;
display: flex;
align-items: center;
justify-content: space-around;
background: rgba(30, 37, 72, 0.8);
backdrop-filter: blur(20rpx);
border-radius: 24rpx;
padding: 30rpx 20rpx;
border: 1rpx solid rgba(107, 122, 255, 0.1);
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.stat-value {
font-size: 40rpx;
font-weight: 700;
color: @text-primary;
line-height: 1.2;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: @text-secondary;
line-height: 1.4;
}
.stat-divider {
width: 1rpx;
height: 60rpx;
background: linear-gradient(to bottom, transparent, @divider-color, transparent);
}
// ========== 筛选区域 ==========
.filter-section {
padding: 20rpx 0;
background-color: @bg-primary;
background-color: @bg-secondary;
border-bottom: 1rpx solid @border-color;
}
.filter-scroll {
@@ -114,28 +45,33 @@ page {
.filter-list {
display: inline-flex;
padding: 0 20rpx;
gap: 16rpx;
padding: 0 24rpx;
gap: 20rpx;
}
.filter-item {
display: inline-flex;
align-items: center;
padding: 16rpx 28rpx;
background-color: @bg-card;
padding: 16rpx 32rpx;
background-color: @bg-primary;
border-radius: 32rpx;
border: 1rpx solid transparent;
border: 1rpx solid @border-color;
transition: all 0.3s ease;
&.active {
background: linear-gradient(135deg, @accent-gradient-start, @accent-gradient-end);
border-color: transparent;
box-shadow: 0 4rpx 16rpx rgba(76, 110, 245, 0.3);
box-shadow: 0 4rpx 16rpx rgba(76, 110, 245, 0.25);
.filter-text {
color: @text-primary;
color: #ffffff;
font-weight: 600;
}
.filter-badge {
background-color: #ffffff;
color: @accent-primary;
}
}
}
@@ -156,7 +92,7 @@ page {
background-color: @status-cancelled;
border-radius: 16rpx;
font-size: 20rpx;
color: @text-primary;
color: #ffffff;
font-weight: 600;
}
@@ -168,7 +104,7 @@ page {
.order-scroll {
height: 100%;
padding: 0 20rpx;
padding: 0 24rpx;
}
.loading-container,
@@ -193,7 +129,7 @@ page {
margin-top: 30rpx;
padding: 16rpx 48rpx;
background: linear-gradient(135deg, @accent-gradient-start, @accent-gradient-end);
color: @text-primary;
color: #ffffff;
font-size: 28rpx;
border-radius: 32rpx;
display: inline-block;
@@ -201,7 +137,7 @@ page {
// ========== 订单卡片 ==========
.order-cards {
padding-bottom: 40rpx;
padding: 24rpx 0 40rpx;
}
.order-card {
@@ -209,34 +145,13 @@ page {
background-color: @bg-card;
border-radius: 24rpx;
border: 1rpx solid @border-color;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:active {
transform: scale(0.98);
background-color: @bg-card-hover;
}
// 状态左边框
&.pending {
border-left: 4rpx solid @status-pending;
}
&.confirmed {
border-left: 4rpx solid @status-confirmed;
}
&.in_progress {
border-left: 4rpx solid @status-in-progress;
}
&.completed {
border-left: 4rpx solid @status-completed;
}
&.cancelled {
border-left: 4rpx solid @status-cancelled;
opacity: 0.85;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
}
@@ -244,7 +159,7 @@ page {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 28rpx 16rpx;
padding: 28rpx 28rpx 20rpx;
}
.order-id {
@@ -256,38 +171,39 @@ page {
.order-id-text {
font-size: 24rpx;
color: @text-muted;
font-weight: 500;
}
.status-tag {
display: inline-flex;
align-items: center;
padding: 6rpx 16rpx;
border-radius: 8rpx;
padding: 8rpx 18rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 600;
&.pending {
background-color: rgba(245, 159, 0, 0.15);
background-color: rgba(245, 159, 0, 0.1);
color: @status-pending;
}
&.confirmed {
background-color: rgba(76, 110, 245, 0.15);
color: @accent-secondary;
background-color: rgba(76, 110, 245, 0.1);
color: @accent-primary;
}
&.in_progress {
background-color: rgba(32, 201, 151, 0.15);
background-color: rgba(32, 201, 151, 0.1);
color: @status-in-progress;
}
&.completed {
background-color: rgba(81, 207, 102, 0.15);
background-color: rgba(81, 207, 102, 0.1);
color: @status-completed;
}
&.cancelled {
background-color: rgba(255, 107, 107, 0.15);
background-color: rgba(255, 107, 107, 0.1);
color: @status-cancelled;
}
}
@@ -298,72 +214,75 @@ page {
}
.patient-row {
margin-bottom: 16rpx;
margin-bottom: 20rpx;
}
.patient-info {
display: flex;
align-items: center;
gap: 20rpx;
gap: 24rpx;
}
.patient-avatar {
width: 72rpx;
height: 72rpx;
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, @accent-gradient-start, @accent-gradient-end);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(76, 110, 245, 0.25);
}
.avatar-text {
font-size: 28rpx;
font-size: 30rpx;
font-weight: 600;
color: @text-primary;
color: #ffffff;
}
.patient-detail {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
gap: 8rpx;
}
.patient-name-row {
display: flex;
align-items: center;
gap: 12rpx;
gap: 14rpx;
}
.patient-name {
font-size: 30rpx;
font-size: 32rpx;
font-weight: 600;
color: @text-primary;
}
.patient-gender {
font-size: 22rpx;
padding: 2rpx 10rpx;
border-radius: 6rpx;
background-color: rgba(107, 122, 255, 0.15);
color: @accent-secondary;
padding: 4rpx 12rpx;
border-radius: 10rpx;
background-color: rgba(76, 110, 245, 0.1);
color: @accent-primary;
font-weight: 500;
&.male {
background-color: rgba(76, 110, 245, 0.15);
background-color: rgba(76, 110, 245, 0.1);
color: @accent-primary;
}
&.female {
background-color: rgba(255, 107, 107, 0.15);
background-color: rgba(255, 107, 107, 0.1);
color: @status-cancelled;
}
}
.patient-age {
font-size: 22rpx;
font-size: 24rpx;
color: @text-secondary;
font-weight: 500;
}
.patient-phone {
@@ -373,25 +292,25 @@ page {
.info-divider {
height: 1rpx;
background: linear-gradient(to right, transparent, @divider-color, transparent);
margin: 16rpx 0;
background-color: @divider-color;
margin: 20rpx 0;
}
.info-item {
display: flex;
align-items: flex-start;
gap: 12rpx;
margin-bottom: 12rpx;
gap: 14rpx;
margin-bottom: 14rpx;
}
.info-content {
display: flex;
flex-direction: column;
gap: 4rpx;
gap: 6rpx;
}
.hospital-name {
font-size: 26rpx;
font-size: 28rpx;
color: @text-primary;
font-weight: 500;
}
@@ -404,16 +323,19 @@ page {
.service-name {
font-size: 26rpx;
color: @text-secondary;
font-weight: 500;
}
.time-text {
font-size: 26rpx;
color: @text-secondary;
font-weight: 500;
}
.attendant-name {
font-size: 26rpx;
color: @text-secondary;
font-weight: 500;
}
// ========== 卡片底部 ==========
@@ -421,9 +343,9 @@ page {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 28rpx 24rpx;
margin-top: 8rpx;
border-top: 1rpx solid rgba(42, 51, 102, 0.5);
padding: 24rpx 28rpx 28rpx;
margin-top: 12rpx;
border-top: 1rpx solid @divider-color;
}
.fee-section {
@@ -438,7 +360,7 @@ page {
}
.fee-value {
font-size: 36rpx;
font-size: 38rpx;
font-weight: 700;
color: @status-pending;
}
@@ -452,7 +374,7 @@ page {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 14rpx 32rpx;
padding: 16rpx 36rpx;
border-radius: 28rpx;
font-size: 26rpx;
font-weight: 500;
@@ -466,17 +388,17 @@ page {
.btn-primary {
background: linear-gradient(135deg, @accent-gradient-start, @accent-gradient-end);
color: @text-primary;
box-shadow: 0 4rpx 16rpx rgba(76, 110, 245, 0.3);
color: #ffffff;
box-shadow: 0 4rpx 16rpx rgba(76, 110, 245, 0.25);
}
.btn-secondary {
background-color: transparent;
background-color: @bg-primary;
color: @text-secondary;
border: 1rpx solid @border-color;
&:active {
background-color: rgba(107, 122, 255, 0.1);
background-color: rgba(76, 110, 245, 0.05);
}
}

View File

@@ -1,66 +1,222 @@
// pages/mine/index.js
const API = require('../../utils/api.js')
Page({
/**
* 页面的初始数据
*/
data: {
isLoggedIn: false,
userInfo: null,
phoneNumber: '',
version: '1.0.0',
showLoginPopup: false,
loginForm: {
name: '',
mobile: ''
},
menuList: [
{ icon: 'user', title: '个人资料', url: '' },
{ icon: 'notification', title: '消息通知', url: '' },
{ icon: 'lock-on', title: '账号安全', url: '' },
{ icon: 'help-circle', title: '帮助中心', url: '' },
{ icon: 'info-circle', title: '关于我们', url: '' },
]
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
onLoad() {
this.checkLoginStatus()
this.setData({ version: this.getAppVersion() })
const app = getApp()
app.eventBus.on('user-login', this.onUserLogin)
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
const app = getApp()
app.eventBus.off('user-login', this.onUserLogin)
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
onShow() {
this.checkLoginStatus()
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
onUserLogin(user) {
this.setData({
isLoggedIn: true,
userInfo: user,
phoneNumber: this.maskPhoneNumber(user.mobile || '')
})
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
checkLoginStatus() {
const app = getApp()
const user = app.globalData.user
if (user && user.security && user.security.token) {
this.setData({
isLoggedIn: true,
userInfo: user,
phoneNumber: this.maskPhoneNumber(user.mobile || '')
})
}
},
maskPhoneNumber(phone) {
if (!phone || phone.length < 7) return phone
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
},
getAppVersion() {
const accountInfo = wx.getAccountInfoSync()
return accountInfo.miniProgram.version || '1.0.0'
},
onShowLoginPopup() {
this.setData({ showLoginPopup: true })
},
onCloseLoginPopup() {
this.setData({ showLoginPopup: false, loginForm: { name: '', mobile: '' } })
},
onNameInput(e) {
this.setData({ 'loginForm.name': e.detail.value })
},
onMobileInput(e) {
this.setData({ 'loginForm.mobile': e.detail.value })
},
onLoginSubmit() {
const { name, mobile } = this.data.loginForm
if (!name.trim()) {
wx.showToast({ title: '请输入姓名', icon: 'none' })
return
}
if (!mobile.trim()) {
wx.showToast({ title: '请输入手机号', icon: 'none' })
return
}
const phoneReg = /^1[3-9]\d{9}$/
if (!phoneReg.test(mobile.trim())) {
wx.showToast({ title: '手机号格式不正确', icon: 'none' })
return
}
wx.showLoading({ title: '登录中...' })
wx.login({
success: (res) => {
if (res.code) {
API.user.wxSignin({ phoneNumber: mobile.trim(), name: name.trim(), code: res.code })
.then((data) => {
if (data.code == 0) {
const app = getApp()
app.globalData.user = data.data.user
this.setData({
isLoggedIn: true,
userInfo: app.globalData.user,
phoneNumber: this.maskPhoneNumber(app.globalData.user.profile.mobile || ''),
showLoginPopup: false,
loginForm: { name: '', mobile: '' }
})
wx.showToast({ title: '登录成功', icon: 'success' })
} else {
wx.showToast({ title: '登录失败', icon: 'none' })
}
wx.hideLoading()
})
} else {
wx.showToast({ title: '登录失败', icon: 'none' })
}
}
}).catch(err => {
console.error('登录失败', err)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
}).finally(() => {
wx.hideLoading()
})
},
onMenuTap(e) {
const { index } = e.currentTarget.dataset
const item = this.data.menuList[index]
if (!this.data.isLoggedIn) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
if (item.url) {
wx.navigateTo({ url: item.url })
} else {
wx.showToast({ title: '功能开发中', icon: 'none' })
}
},
onLogout() {
wx.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
confirmColor: '#4c6ef5',
success: (res) => {
if (res.confirm) {
wx.showLoading({ title: '退出中...' })
API.user.signout().then(() => {
this.doLogout()
}).catch(() => {
this.doLogout()
}).finally(() => {
wx.hideLoading()
})
}
}
})
},
doLogout() {
const app = getApp()
app.globalData.user = null
wx.removeStorageSync('admin_user_info')
wx.removeStorageSync('admin_ai_session_id')
wx.removeStorageSync('admin_ai_chat_history')
this.setData({
isLoggedIn: false,
userInfo: null,
phoneNumber: ''
})
wx.showToast({ title: '已退出登录', icon: 'success' })
},
onPopupContentTap() {
// 阻止冒泡,防止点击弹窗内容时关闭弹窗
},
onClearCache() {
wx.showModal({
title: '清除缓存',
content: '确定要清除所有缓存数据吗?',
confirmColor: '#4c6ef5',
success: (res) => {
if (res.confirm) {
wx.clearStorage({
success: () => {
const app = getApp()
app.globalData.user = null
this.setData({
isLoggedIn: false,
userInfo: null,
phoneNumber: ''
})
wx.showToast({ title: '缓存已清除', icon: 'success' })
}
})
}
}
})
}
})
})

View File

@@ -1,3 +1,9 @@
{
"usingComponents": {}
}
"navigationBarTitleText": "设置",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#ffffff",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

View File

@@ -1,2 +1,91 @@
<!--pages/mine/index.wxml-->
<text>pages/mine/index.wxml</text>
<view class="set-page">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="user-info" wx:if="{{isLoggedIn}}">
<view class="user-avatar">
<text class="avatar-text">{{userInfo.nickname ? userInfo.nickname[0] : '管'}}</text>
</view>
<view class="user-detail">
<text class="user-name">{{userInfo.nickname || '管理员'}}</text>
<text class="user-phone">{{phoneNumber}}</text>
</view>
<view class="logout-btn" bindtap="onLogout">
<text class="logout-text">退出</text>
</view>
</view>
<view class="user-info login-area" wx:else>
<view class="user-avatar default-avatar">
<t-icon name="user" size="40rpx" color="#9ca3af" />
</view>
<view class="user-detail">
<text class="login-title">未登录</text>
<text class="login-desc">登录后查看更多信息</text>
</view>
<view class="login-btn" bindtap="onShowLoginPopup">
<text class="login-btn-text">立即登录</text>
</view>
</view>
</view>
<!-- 菜单列表 -->
<view class="menu-section">
<view class="menu-list">
<view class="menu-item" wx:for="{{menuList}}" wx:key="index" data-index="{{index}}" bindtap="onMenuTap">
<view class="menu-left">
<t-icon name="{{item.icon}}" size="32rpx" color="#4c6ef5" />
<text class="menu-title">{{item.title}}</text>
</view>
<view class="menu-right">
<t-icon name="chevron-right" size="28rpx" color="#9ca3af" />
</view>
</view>
</view>
</view>
<!-- 系统操作 -->
<view class="menu-section">
<view class="menu-list">
<view class="menu-item" bindtap="onClearCache">
<view class="menu-left">
<t-icon name="delete" size="32rpx" color="#ff6b6b" />
<text class="menu-title danger">清除缓存</text>
</view>
<view class="menu-right">
<t-icon name="chevron-right" size="28rpx" color="#9ca3af" />
</view>
</view>
</view>
</view>
<!-- 登录弹窗 -->
<view class="login-popup" wx:if="{{showLoginPopup}}" bindtap="onCloseLoginPopup">
<view class="login-popup-content" catchtap="onPopupContentTap">
<view class="popup-header">
<text class="popup-title">用户登录</text>
<view class="popup-close" bindtap="onCloseLoginPopup">
<t-icon name="close" size="32rpx" color="#9ca3af" />
</view>
</view>
<view class="popup-body">
<view class="form-item">
<text class="form-label">姓名</text>
<input class="form-input" type="text" placeholder="请输入您的姓名" value="{{loginForm.name}}" bindinput="onNameInput" />
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<input class="form-input" type="number" maxlength="11" placeholder="请输入您的手机号" value="{{loginForm.mobile}}" bindinput="onMobileInput" />
</view>
</view>
<view class="popup-footer">
<view class="submit-btn" bindtap="onLoginSubmit">
<text class="submit-btn-text">登录</text>
</view>
</view>
</view>
</view>
<!-- 版本信息 -->
<view class="version-info">
<text class="version-text">版本 {{version}}</text>
</view>
</view>

View File

@@ -1 +1,303 @@
/* pages/mine/index.wxss */
page {
background-color: #f5f6fa;
color: #1a1a2e;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.set-page {
padding: 24rpx;
}
.user-card {
background-color: #ffffff;
border-radius: 24rpx;
border: 1rpx solid #e5e7eb;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
margin-bottom: 24rpx;
}
.user-info {
display: flex;
align-items: center;
padding: 40rpx 32rpx;
gap: 24rpx;
}
.user-info.login-area {
align-items: center;
}
.user-avatar {
width: 100rpx;
height: 100rpx;
background: linear-gradient(135deg, #4c6ef5, #748ffc);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(76, 110, 245, 0.25);
}
.user-avatar.default-avatar {
background: #f3f4f6;
box-shadow: none;
}
.avatar-text {
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
}
.user-detail {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.user-name {
font-size: 32rpx;
font-weight: 600;
color: #1a1a2e;
}
.user-phone {
font-size: 26rpx;
color: #9ca3af;
}
.login-title {
font-size: 32rpx;
font-weight: 600;
color: #1a1a2e;
}
.login-desc {
font-size: 24rpx;
color: #9ca3af;
}
.logout-btn {
padding: 12rpx 28rpx;
background-color: rgba(255, 107, 107, 0.1);
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
}
.logout-btn:active {
transform: scale(0.95);
opacity: 0.9;
}
.logout-text {
font-size: 24rpx;
color: #ff6b6b;
font-weight: 500;
}
.login-btn {
margin: 0;
padding: 16rpx 32rpx;
background: linear-gradient(135deg, #4c6ef5, #748ffc);
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
border: none;
}
.login-btn::after {
border: none;
}
.login-btn:active {
transform: scale(0.95);
opacity: 0.9;
}
.login-btn-text {
font-size: 26rpx;
color: #ffffff;
font-weight: 500;
}
.menu-section {
background-color: #ffffff;
border-radius: 24rpx;
border: 1rpx solid #e5e7eb;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
margin-bottom: 24rpx;
}
.menu-list {
padding: 0 32rpx;
}
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 0;
border-bottom: 1rpx solid #f3f4f6;
transition: background-color 0.2s ease;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background-color: rgba(76, 110, 245, 0.05);
}
.menu-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.menu-title {
font-size: 28rpx;
color: #1a1a2e;
font-weight: 500;
}
.menu-title.danger {
color: #ff6b6b;
}
.menu-right {
display: flex;
align-items: center;
}
.version-info {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 0;
}
.version-text {
font-size: 24rpx;
color: #9ca3af;
}
/* 登录弹窗 */
.login-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.login-popup-content {
width: 80%;
max-width: 600rpx;
background-color: #ffffff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 32rpx 0;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #1a1a2e;
}
.popup-close {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.popup-close:active {
background-color: #f3f4f6;
}
.popup-body {
padding: 32rpx;
}
.form-item {
margin-bottom: 24rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 26rpx;
color: #6b7280;
margin-bottom: 12rpx;
font-weight: 500;
}
.form-input {
width: 100%;
height: 80rpx;
background-color: #f5f6fa;
border-radius: 16rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #1a1a2e;
box-sizing: border-box;
border: 1rpx solid #e5e7eb;
}
.form-input:focus {
border-color: #4c6ef5;
}
.popup-footer {
padding: 0 32rpx 32rpx;
}
.submit-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #4c6ef5, #748ffc);
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(76, 110, 245, 0.25);
transition: all 0.2s ease;
}
.submit-btn:active {
transform: scale(0.98);
opacity: 0.9;
}
.submit-btn-text {
font-size: 30rpx;
color: #ffffff;
font-weight: 600;
}

View File

@@ -3,7 +3,7 @@
"projectname": "wxapp_admin",
"condition": {},
"setting": {
"urlCheck": true,
"urlCheck": false,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,

View File

@@ -6,6 +6,8 @@ const API = {
wxSignin: (data) => request.post('/user/wxsignin', data),
signout: (data) => request.post('/user/signout', data),
update: (data) => request.post('/user/update', data),
userInfo: (data) => request.post('/user/userInfo', data),
userList: (data) => request.post('/user/list', data),
},
escort: {
@@ -15,6 +17,7 @@ const API = {
createRecord: (data) => request.post('/health/escort-record', data),
updateRecord: (id, data) => request.put(`/health/escort-record/${id}`, data),
updateStatus: (id, data) => request.patch(`/health/escort-record/${id}/status`, data),
deleteRecord: (id) => request.delete(`/health/escort-record/${id}`),
},
resource: {

View File

@@ -12,7 +12,8 @@ class Request {
const app = getApp()
const token = app?.globalData?.user?.security?.token || ''
data.appId = 'wxapp-escort'
data.appId = 'wxapp-escort-admin'
data.token = token
wx.request({
url: url.startsWith('http') ? url : `${this.baseURL}${url}`,
@@ -45,6 +46,18 @@ class Request {
post(url, data = {}, options = {}) {
return this.request({ url, method: 'POST', data, ...options })
}
put(url, data = {}, options = {}) {
return this.request({ url, method: 'PUT', data, ...options })
}
patch(url, data = {}, options = {}) {
return this.request({ url, method: 'PATCH', data, ...options })
}
delete(url, data = {}, options = {}) {
return this.request({ url, method: 'DELETE', data, ...options })
}
}
const request = new Request()