486 lines
33 KiB
HTML
486 lines
33 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}通讯录管理 - 机器人管理后台{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-shell contacts-page">
|
|
<div class="page-hero">
|
|
<div class="page-hero-copy">
|
|
<div class="page-eyebrow">Contacts Workspace</div>
|
|
<h1>通讯录管理</h1>
|
|
<p>把联系人、群组、公众号和公共好友统一放进一个更现代的联络工作台里。</p>
|
|
</div>
|
|
<div class="page-hero-actions">
|
|
<el-input placeholder="搜索联系人 / 群组..." v-model="searchQuery" class="hero-search" clearable>
|
|
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
|
</el-input>
|
|
<el-button type="success" @click="updateContacts">更新通讯录</el-button>
|
|
<el-button type="primary" @click="refreshContacts">刷新数据</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<el-row :gutter="16" class="overview-grid">
|
|
<el-col :span="6">
|
|
<el-card class="overview-card overview-card--primary" shadow="hover">
|
|
<div class="overview-label">总联系人数</div>
|
|
<div class="overview-value">{% raw %}{{ statistics.total }}{% endraw %}</div>
|
|
<div class="overview-note">系统已收录联系人总量</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-card class="overview-card" shadow="hover">
|
|
<div class="overview-label">群组数</div>
|
|
<div class="overview-value">{% raw %}{{ statistics.groups }}{% endraw %}</div>
|
|
<div class="overview-note">活跃或可管理的群组</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-card class="overview-card" shadow="hover">
|
|
<div class="overview-label">个人联系人数</div>
|
|
<div class="overview-value">{% raw %}{{ statistics.personal }}{% endraw %}</div>
|
|
<div class="overview-note">私聊联系人规模</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-card class="overview-card overview-card--soft" shadow="hover">
|
|
<div class="overview-label">公众号数</div>
|
|
<div class="overview-value">{% raw %}{{ statistics.official }}{% endraw %}</div>
|
|
<div class="overview-note">已同步的公众号账号</div>
|
|
</el-card>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-card class="workspace-card" shadow="hover">
|
|
<div slot="header" class="workspace-header">
|
|
<div>
|
|
<h3>联系人分类视图</h3>
|
|
<p>按群组、个人、公众号和公共好友切换查看,减少跳页成本。</p>
|
|
</div>
|
|
</div>
|
|
|
|
<el-tabs v-model="activeTab" @tab-click="handleTabClick" class="contacts-tabs">
|
|
<el-tab-pane label="群组" name="groups">
|
|
<el-table :data="filteredGroups" style="width: 100%">
|
|
<el-table-column type="index" width="54"></el-table-column>
|
|
<el-table-column label="群组信息" min-width="320">
|
|
<template slot-scope="scope">
|
|
<div class="entity-cell">
|
|
<div class="entity-avatar entity-avatar--group">
|
|
<i class="el-icon-user-solid"></i>
|
|
</div>
|
|
<div class="entity-copy">
|
|
<div class="entity-title">{% raw %}{{ scope.row.name }}{% endraw %}</div>
|
|
<div class="entity-subtitle">{% raw %}{{ scope.row.wxid }}{% endraw %}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="220">
|
|
<template slot-scope="scope">
|
|
<div class="action-row">
|
|
<el-button size="mini" type="primary" plain @click="viewGroupDetails(scope.row)">查看详情</el-button>
|
|
<el-button size="mini" type="success" @click="openChatDialog(scope.row)">聊天</el-button>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane label="个人联系人" name="personal">
|
|
<el-table :data="filteredPersonal" style="width: 100%">
|
|
<el-table-column type="index" width="54"></el-table-column>
|
|
<el-table-column label="联系人" min-width="320">
|
|
<template slot-scope="scope">
|
|
<div class="entity-cell">
|
|
<el-avatar size="small" :src="getHeadImage(scope.row.wxid)" @error="() => true">
|
|
<img src="/static/logo.png"/>
|
|
</el-avatar>
|
|
<div class="entity-copy">
|
|
<div class="entity-title">{% raw %}{{ scope.row.name }}{% endraw %}</div>
|
|
<div class="entity-subtitle">{% raw %}{{ scope.row.wxid }}{% endraw %}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="220">
|
|
<template slot-scope="scope">
|
|
<div class="action-row">
|
|
<el-button size="mini" type="primary" plain @click="viewUserDetails(scope.row)">查看详情</el-button>
|
|
<el-button size="mini" type="success" @click="openChatDialog(scope.row)">聊天</el-button>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane label="公众号" name="official">
|
|
<el-table :data="filteredOfficial" style="width: 100%">
|
|
<el-table-column type="index" width="54"></el-table-column>
|
|
<el-table-column label="公众号" min-width="320">
|
|
<template slot-scope="scope">
|
|
<div class="entity-cell">
|
|
<el-avatar size="small" :src="getHeadImage(scope.row.wxid)" @error="() => true">
|
|
<img src="/static/logo.png"/>
|
|
</el-avatar>
|
|
<div class="entity-copy">
|
|
<div class="entity-title">{% raw %}{{ scope.row.name }}{% endraw %}</div>
|
|
<div class="entity-subtitle">{% raw %}{{ scope.row.wxid }}{% endraw %}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="160">
|
|
<template slot-scope="scope">
|
|
<el-button size="mini" type="primary" plain @click="viewOfficialDetails(scope.row)">查看详情</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane label="公共好友" name="public">
|
|
<el-table :data="filteredPublic" style="width: 100%">
|
|
<el-table-column type="index" width="54"></el-table-column>
|
|
<el-table-column label="好友信息" min-width="320">
|
|
<template slot-scope="scope">
|
|
<div class="entity-cell">
|
|
<el-avatar size="small" :src="getHeadImage(scope.row.wxid)" @error="() => true">
|
|
<img src="/static/logo.png"/>
|
|
</el-avatar>
|
|
<div class="entity-copy">
|
|
<div class="entity-title">{% raw %}{{ scope.row.name }}{% endraw %}</div>
|
|
<div class="entity-subtitle">{% raw %}{{ scope.row.wxid }}{% endraw %}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="160">
|
|
<template slot-scope="scope">
|
|
<el-button size="mini" type="primary" plain @click="viewPublicDetails(scope.row)">查看详情</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-tab-pane>
|
|
</el-tabs>
|
|
|
|
<div class="pagination-container" v-if="activeTab === 'groups' && groupsList.length > 10">
|
|
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="currentPage" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="groupsList.length"></el-pagination>
|
|
</div>
|
|
<div class="pagination-container" v-if="activeTab === 'personal' && personalList.length > 10">
|
|
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="currentPage" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="personalList.length"></el-pagination>
|
|
</div>
|
|
<div class="pagination-container" v-if="activeTab === 'official' && officialList.length > 10">
|
|
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="currentPage" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="officialList.length"></el-pagination>
|
|
</div>
|
|
<div class="pagination-container" v-if="activeTab === 'public' && publicList.length > 10">
|
|
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="currentPage" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="publicList.length"></el-pagination>
|
|
</div>
|
|
</el-card>
|
|
|
|
<el-dialog title="群组详情" :visible.sync="groupDetailDialogVisible" width="72%">
|
|
<el-descriptions :column="1" border>
|
|
<el-descriptions-item label="群ID">{% raw %}{{ currentGroup.wxid }}{% endraw %}</el-descriptions-item>
|
|
<el-descriptions-item label="群名称">{% raw %}{{ currentGroup.name }}{% endraw %}</el-descriptions-item>
|
|
</el-descriptions>
|
|
|
|
<div class="group-members-section">
|
|
<div class="section-title">
|
|
<h3>群成员列表</h3>
|
|
<el-input placeholder="搜索群成员..." v-model="groupMemberSearchQuery" class="group-search" clearable></el-input>
|
|
</div>
|
|
<el-table :data="filteredGroupMembers" style="width: 100%" v-loading="groupMembersLoading">
|
|
<el-table-column type="index" width="54"></el-table-column>
|
|
<el-table-column label="成员" min-width="300">
|
|
<template slot-scope="scope">
|
|
<div class="entity-cell">
|
|
<el-avatar size="small" :src="getHeadImage(scope.row.wxid)" @error="() => true">
|
|
<img src="/static/logo.png"/>
|
|
</el-avatar>
|
|
<div class="entity-copy">
|
|
<div class="entity-title">{% raw %}{{ scope.row.name || '-' }}{% endraw %}</div>
|
|
<div class="entity-subtitle">{% raw %}{{ scope.row.wxid }}{% endraw %}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="display_name" label="群昵称" width="240"></el-table-column>
|
|
<el-table-column label="状态" width="100" align="center">
|
|
<template slot-scope="scope">
|
|
<el-tag size="mini" :type="scope.row.status === 1 ? 'success' : 'info'">
|
|
{% raw %}{{ scope.row.status === 1 ? '在群' : '已退群' }}{% endraw %}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="latest_active_time" label="活跃时间">
|
|
<template slot-scope="scope">{% raw %}{{ scope.row.latest_active_time || '-' }}{% endraw %}</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<div class="pagination-container" v-if="groupMembersList.length > 10">
|
|
<el-pagination @size-change="handleGroupMembersSizeChange" @current-change="handleGroupMembersCurrentChange" :current-page="groupMembersCurrentPage" :page-sizes="[10, 20, 50, 100]" :page-size="groupMembersPageSize" layout="total, sizes, prev, pager, next, jumper" :total="groupMembersList.length"></el-pagination>
|
|
</div>
|
|
</div>
|
|
</el-dialog>
|
|
|
|
<el-dialog title="用户详情" :visible.sync="userDetailDialogVisible" width="50%">
|
|
<div class="detail-avatar-wrap">
|
|
<el-avatar size="large" :src="getHeadImage(currentUser.wxid)" @error="() => true" class="detail-avatar">
|
|
<img src="/static/logo.png"/>
|
|
</el-avatar>
|
|
</div>
|
|
<el-descriptions :column="1" border>
|
|
<el-descriptions-item label="微信ID">{% raw %}{{ currentUser.wxid }}{% endraw %}</el-descriptions-item>
|
|
<el-descriptions-item label="昵称">{% raw %}{{ currentUser.name }}{% endraw %}</el-descriptions-item>
|
|
</el-descriptions>
|
|
</el-dialog>
|
|
|
|
<el-dialog title="公众号详情" :visible.sync="officialDetailDialogVisible" width="50%">
|
|
<div class="detail-avatar-wrap">
|
|
<el-avatar size="large" :src="getHeadImage(currentOfficial.wxid)" @error="() => true" class="detail-avatar">
|
|
<img src="/static/logo.png"/>
|
|
</el-avatar>
|
|
</div>
|
|
<el-descriptions :column="1" border>
|
|
<el-descriptions-item label="公众号ID">{% raw %}{{ currentOfficial.wxid }}{% endraw %}</el-descriptions-item>
|
|
<el-descriptions-item label="公众号名称">{% raw %}{{ currentOfficial.name }}{% endraw %}</el-descriptions-item>
|
|
</el-descriptions>
|
|
</el-dialog>
|
|
|
|
<el-dialog title="公共好友详情" :visible.sync="publicDetailDialogVisible" width="50%">
|
|
<div class="detail-avatar-wrap">
|
|
<el-avatar size="large" :src="currentPublic.small_head_img_url" @error="() => true" class="detail-avatar">
|
|
<img src="/static/logo.png"/>
|
|
</el-avatar>
|
|
</div>
|
|
<el-descriptions :column="1" border>
|
|
<el-descriptions-item label="ID">{% raw %}{{ currentPublic.wxid }}{% endraw %}</el-descriptions-item>
|
|
<el-descriptions-item label="名称">{% raw %}{{ currentPublic.name }}{% endraw %}</el-descriptions-item>
|
|
</el-descriptions>
|
|
</el-dialog>
|
|
|
|
<el-dialog title="聊天" :visible.sync="chatDialogVisible" width="60%" :close-on-click-modal="true">
|
|
<div class="chat-container">
|
|
<div class="message-list" ref="messageList">
|
|
{% raw %}
|
|
<div v-for="(msg, index) in chatMessages" :key="index" class="message-item" :class="{'message-self': msg.isSelf}">
|
|
<div class="message-content">
|
|
<div v-if="msg.type === 'text'" v-text="msg.content"></div>
|
|
<div v-else-if="msg.type === 'image'"><img :src="msg.content" style="max-width: 200px; max-height: 200px;"></div>
|
|
<div v-else-if="msg.type === 'voice'"><audio controls :src="msg.content"></audio></div>
|
|
<div v-else-if="msg.type === 'video'"><video controls :src="msg.content" style="max-width: 200px;"></video></div>
|
|
<div v-else-if="msg.type === 'link'"><a :href="msg.content.url" target="_blank" v-text="msg.content.title"></a><p v-text="msg.content.description"></p></div>
|
|
</div>
|
|
<div class="message-time" v-text="msg.time"></div>
|
|
</div>
|
|
{% endraw %}
|
|
</div>
|
|
<div class="input-area">
|
|
<el-input type="textarea" :rows="3" placeholder="请输入消息..." v-model="messageInput" @keyup.enter.native="sendTextMessage"></el-input>
|
|
<div class="toolbar">
|
|
<el-upload class="upload-demo" action="#" :http-request="uploadImage" :show-file-list="false"><el-button size="small" type="primary">图片</el-button></el-upload>
|
|
<el-upload class="upload-demo" action="#" :http-request="uploadVoice" :show-file-list="false"><el-button size="small" type="primary">语音</el-button></el-upload>
|
|
<el-upload class="upload-demo" action="#" :http-request="uploadVideo" :show-file-list="false"><el-button size="small" type="primary">视频</el-button></el-upload>
|
|
<el-button size="small" type="primary" @click="showLinkDialog">链接</el-button>
|
|
<el-button size="small" type="success" @click="sendTextMessage">发送</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</el-dialog>
|
|
|
|
<el-dialog title="发送链接" :visible.sync="linkDialogVisible" width="34%">
|
|
<el-form :model="linkForm" label-width="80px">
|
|
<el-form-item label="链接"><el-input v-model="linkForm.url" placeholder="请输入链接"></el-input></el-form-item>
|
|
<el-form-item label="标题"><el-input v-model="linkForm.title" placeholder="请输入标题"></el-input></el-form-item>
|
|
<el-form-item label="描述"><el-input type="textarea" v-model="linkForm.description" placeholder="请输入描述"></el-input></el-form-item>
|
|
</el-form>
|
|
<span slot="footer" class="dialog-footer">
|
|
<el-button @click="linkDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="sendLinkMessage">发送</el-button>
|
|
</span>
|
|
</el-dialog>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
new Vue({
|
|
el: '#app',
|
|
mixins: [baseApp],
|
|
data() {
|
|
return {
|
|
activeTab: 'groups',
|
|
searchQuery: '',
|
|
currentPage: 1,
|
|
pageSize: 10,
|
|
groupsList: [],
|
|
personalList: [],
|
|
officialList: [],
|
|
publicList: [],
|
|
headImages: {},
|
|
statistics: { total: 0, groups: 0, personal: 0, official: 0, public: 0 },
|
|
groupDetailDialogVisible: false,
|
|
userDetailDialogVisible: false,
|
|
officialDetailDialogVisible: false,
|
|
publicDetailDialogVisible: false,
|
|
currentGroup: {}, currentUser: {}, currentOfficial: {}, currentPublic: {},
|
|
groupMembersList: [], groupMembersCurrentPage: 1, groupMembersPageSize: 10, groupMemberSearchQuery: '', groupMembersLoading: false,
|
|
chatDialogVisible: false, currentChatUser: null, messageInput: '', chatMessages: [],
|
|
linkDialogVisible: false,
|
|
linkForm: { url: '', title: '', description: '' }
|
|
};
|
|
},
|
|
computed: {
|
|
filteredGroups() {
|
|
const query = this.searchQuery.toLowerCase();
|
|
const list = !query ? this.groupsList : this.groupsList.filter(group => group.wxid.toLowerCase().includes(query) || group.name.toLowerCase().includes(query));
|
|
return list.slice((this.currentPage - 1) * this.pageSize, this.currentPage * this.pageSize);
|
|
},
|
|
filteredPersonal() {
|
|
const query = this.searchQuery.toLowerCase();
|
|
const list = !query ? this.personalList : this.personalList.filter(user => user.wxid.toLowerCase().includes(query) || user.name.toLowerCase().includes(query));
|
|
return list.slice((this.currentPage - 1) * this.pageSize, this.currentPage * this.pageSize);
|
|
},
|
|
filteredOfficial() {
|
|
const query = this.searchQuery.toLowerCase();
|
|
const list = !query ? this.officialList : this.officialList.filter(official => official.wxid.toLowerCase().includes(query) || official.name.toLowerCase().includes(query));
|
|
return list.slice((this.currentPage - 1) * this.pageSize, this.currentPage * this.pageSize);
|
|
},
|
|
filteredPublic() {
|
|
const query = this.searchQuery.toLowerCase();
|
|
const list = !query ? this.publicList : this.publicList.filter(item => item.wxid.toLowerCase().includes(query) || item.name.toLowerCase().includes(query));
|
|
return list.slice((this.currentPage - 1) * this.pageSize, this.currentPage * this.pageSize);
|
|
},
|
|
filteredGroupMembers() {
|
|
const query = this.groupMemberSearchQuery.toLowerCase();
|
|
const list = !query ? this.groupMembersList : this.groupMembersList.filter(member => member.wxid.toLowerCase().includes(query) || (member.name && member.name.toLowerCase().includes(query)));
|
|
return list.slice((this.groupMembersCurrentPage - 1) * this.groupMembersPageSize, this.groupMembersCurrentPage * this.groupMembersPageSize);
|
|
}
|
|
},
|
|
mounted() {
|
|
this.currentView = '10';
|
|
this.loadContactsData();
|
|
},
|
|
methods: {
|
|
loadContactsData() {
|
|
axios.get('/contacts/api/statistics').then(response => { if (response.data.success) this.statistics = response.data.data; }).catch(error => { console.error('加载联系人统计数据失败:', error); this.$message.error('加载联系人统计数据失败'); });
|
|
axios.get('/contacts/api/groups').then(response => { if (response.data.success) { const groups = response.data.data.groups; this.groupsList = Object.entries(groups).map(([wxid, name]) => ({ wxid, name })); } }).catch(error => { console.error('加载群组数据失败:', error); this.$message.error('加载群组数据失败'); });
|
|
axios.get('/contacts/api/personal').then(response => { if (response.data.success) { const personal = response.data.data.personal; this.personalList = Object.entries(personal).map(([wxid, name]) => ({ wxid, name })); } }).catch(error => { console.error('加载个人联系人数据失败:', error); this.$message.error('加载个人联系人数据失败'); });
|
|
axios.get('/contacts/api/official').then(response => { if (response.data.success) { const official = response.data.data.official; this.officialList = Object.entries(official).map(([wxid, name]) => ({ wxid, name })); } }).catch(error => { console.error('加载公众号数据失败:', error); this.$message.error('加载公众号数据失败'); });
|
|
axios.get('/contacts/api/public').then(response => { if (response.data.success) { const publicFriends = response.data.data.public; this.publicList = Object.entries(publicFriends).map(([wxid, name]) => ({ wxid, name })); } }).catch(error => { console.error('加载公共好友数据失败:', error); this.$message.error('加载公共好友数据失败'); });
|
|
axios.get('/contacts/api/head_images').then(response => { if (response.data.success) this.headImages = response.data.data.head_images; }).catch(error => { console.error('加载联系人头像数据失败:', error); this.$message.error('加载联系人头像数据失败'); });
|
|
},
|
|
updateContacts() {
|
|
this.$message.info('正在更新通讯录...');
|
|
axios.post('/contacts/api/update').then(res => { if (res.data.success) { this.$message.success('通讯录更新成功!'); this.refreshContacts(); } else { this.$message.error(res.data.message || '通讯录更新失败'); } }).catch(() => { this.$message.error('通讯录更新请求失败'); });
|
|
},
|
|
refreshContacts() { this.loadContactsData(); this.$message.success('联系人数据已刷新'); },
|
|
handleTabClick() { this.currentPage = 1; },
|
|
getHeadImage(wxid) { return this.headImages[wxid] || ''; },
|
|
handleSizeChange(size) { this.pageSize = size; },
|
|
handleCurrentChange(page) { this.currentPage = page; },
|
|
viewGroupDetails(group) { this.currentGroup = group; this.groupDetailDialogVisible = true; this.loadGroupMembers(group.wxid); },
|
|
viewUserDetails(user) { this.currentUser = user; this.userDetailDialogVisible = true; },
|
|
viewOfficialDetails(official) { this.currentOfficial = official; this.officialDetailDialogVisible = true; },
|
|
viewPublicDetails(publicFriend) { this.currentPublic = publicFriend; this.publicDetailDialogVisible = true; },
|
|
loadGroupMembers(roomid) {
|
|
this.groupMembersLoading = true; this.groupMembersList = []; this.groupMembersCurrentPage = 1;
|
|
axios.get(`/contacts/api/group_members/${roomid}`).then(response => {
|
|
if (response.data.success) {
|
|
const members = response.data.data.members;
|
|
this.groupMembersList = members.map(item => ({ wxid: item.wxid, name: item.nick_name, display_name: item.display_name, status: item.status, latest_active_time: item.latest_active_time, small_head_img_url: item.small_head_img_url }));
|
|
} else { this.$message.error('获取群成员失败'); }
|
|
}).catch(error => { console.error('加载群成员数据失败:', error); this.$message.error('加载群成员数据失败'); }).finally(() => { this.groupMembersLoading = false; });
|
|
},
|
|
handleGroupMembersSizeChange(size) { this.groupMembersPageSize = size; },
|
|
handleGroupMembersCurrentChange(page) { this.groupMembersCurrentPage = page; },
|
|
openChatDialog(user) { this.currentChatUser = user; this.chatDialogVisible = true; this.chatMessages = []; },
|
|
async sendTextMessage() {
|
|
if (!this.messageInput.trim()) return;
|
|
try {
|
|
const response = await axios.post('/contacts/api/send_message', { wxid: this.currentChatUser.wxid, type: 'text', content: this.messageInput });
|
|
if (response.data.success) { this.chatMessages.push({ type: 'text', content: this.messageInput, isSelf: true, time: new Date().toLocaleTimeString() }); this.messageInput = ''; this.$nextTick(() => { this.scrollToBottom(); }); }
|
|
} catch (error) { this.$message.error('发送消息失败'); }
|
|
},
|
|
async uploadImage(options) {
|
|
const formData = new FormData(); formData.append('file', options.file); formData.append('wxid', this.currentChatUser.wxid); formData.append('type', 'image');
|
|
try { const response = await axios.post('/contacts/api/send_message', formData); if (response.data.success) { this.chatMessages.push({ type: 'image', content: response.data.url, isSelf: true, time: new Date().toLocaleTimeString() }); this.$nextTick(() => { this.scrollToBottom(); }); } } catch (error) { this.$message.error('发送图片失败'); }
|
|
},
|
|
async uploadVoice(options) {
|
|
const formData = new FormData(); formData.append('file', options.file); formData.append('wxid', this.currentChatUser.wxid); formData.append('type', 'voice');
|
|
try { const response = await axios.post('/contacts/api/send_message', formData); if (response.data.success) { this.chatMessages.push({ type: 'voice', content: response.data.url, isSelf: true, time: new Date().toLocaleTimeString() }); this.$nextTick(() => { this.scrollToBottom(); }); } } catch (error) { this.$message.error('发送语音失败'); }
|
|
},
|
|
async uploadVideo(options) {
|
|
const formData = new FormData(); formData.append('file', options.file); formData.append('wxid', this.currentChatUser.wxid); formData.append('type', 'video');
|
|
try { const response = await axios.post('/contacts/api/send_message', formData); if (response.data.success) { this.chatMessages.push({ type: 'video', content: response.data.url, isSelf: true, time: new Date().toLocaleTimeString() }); this.$nextTick(() => { this.scrollToBottom(); }); } } catch (error) { this.$message.error('发送视频失败'); }
|
|
},
|
|
showLinkDialog() { this.linkForm = { url: '', title: '', description: '' }; this.linkDialogVisible = true; },
|
|
async sendLinkMessage() {
|
|
if (!this.linkForm.url) { this.$message.warning('请输入链接'); return; }
|
|
try {
|
|
const response = await axios.post('/contacts/api/send_message', { wxid: this.currentChatUser.wxid, type: 'link', content: this.linkForm });
|
|
if (response.data.success) { this.chatMessages.push({ type: 'link', content: this.linkForm, isSelf: true, time: new Date().toLocaleTimeString() }); this.linkDialogVisible = false; this.$nextTick(() => { this.scrollToBottom(); }); }
|
|
} catch (error) { this.$message.error('发送链接失败'); }
|
|
},
|
|
scrollToBottom() { const messageList = this.$refs.messageList; if (messageList) messageList.scrollTop = messageList.scrollHeight; }
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.page-shell { display: flex; flex-direction: column; gap: 16px; }
|
|
.page-hero {
|
|
display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; padding: 24px 26px; border-radius: 24px;
|
|
background: linear-gradient(135deg, rgba(79,70,229,0.10), rgba(59,130,246,0.08), rgba(255,255,255,0.9));
|
|
border: 1px solid rgba(148, 163, 184, 0.16); box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
|
|
}
|
|
.page-hero-actions { display: flex; align-items: center; gap: 12px; }
|
|
.hero-search { width: 260px; }
|
|
.page-eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #6366f1; font-weight: 700; margin-bottom: 8px; }
|
|
.page-hero-copy h1 { font-size: 30px; line-height: 1.1; margin-bottom: 10px; color: #0f172a; }
|
|
.page-hero-copy p { color: #64748b; font-size: 14px; }
|
|
.overview-grid .el-col { margin-bottom: 16px; }
|
|
.overview-card { min-height: 112px; }
|
|
.overview-card--primary { background: linear-gradient(180deg, rgba(79,70,229,0.10), rgba(255,255,255,0.94)) !important; }
|
|
.overview-card--soft { background: linear-gradient(180deg, rgba(59,130,246,0.08), rgba(255,255,255,0.94)) !important; }
|
|
.overview-label { font-size: 13px; color: #64748b; margin-bottom: 14px; }
|
|
.overview-value { font-size: 30px; font-weight: 700; color: #0f172a; margin-bottom: 10px; }
|
|
.overview-note { font-size: 12px; color: #94a3b8; }
|
|
.workspace-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
.workspace-header h3 { font-size: 18px; margin-bottom: 4px; }
|
|
.workspace-header p { font-size: 13px; color: #64748b; }
|
|
.entity-cell { display: flex; align-items: center; gap: 12px; }
|
|
.entity-avatar {
|
|
width: 32px; height: 32px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
|
|
background: rgba(79,70,229,0.10); color: #4f46e5; font-size: 14px; flex-shrink: 0;
|
|
}
|
|
.entity-avatar--group { background: rgba(16,185,129,0.12); color: #10b981; }
|
|
.entity-title { font-size: 14px; font-weight: 600; color: #0f172a; }
|
|
.entity-subtitle { margin-top: 4px; font-size: 12px; color: #94a3b8; }
|
|
.action-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
.pagination-container { margin-top: 20px; text-align: right; }
|
|
.group-members-section { margin-top: 20px; }
|
|
.section-title {
|
|
margin: 20px 0 15px 0; border-bottom: 1px solid rgba(148,163,184,0.12); padding-bottom: 10px;
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
}
|
|
.section-title h3 { margin: 0; font-size: 18px; color: #0f172a; }
|
|
.group-search { width: 220px; }
|
|
.detail-avatar-wrap { text-align: center; margin-bottom: 20px; }
|
|
.detail-avatar { width: 100px; height: 100px; }
|
|
.chat-container { display: flex; flex-direction: column; height: 500px; }
|
|
.message-list {
|
|
flex: 1; overflow-y: auto; padding: 20px; background: rgba(248,250,252,0.82); border: 1px solid rgba(148,163,184,0.12);
|
|
border-radius: 18px;
|
|
}
|
|
.message-item { margin-bottom: 15px; display: flex; flex-direction: column; }
|
|
.message-self { align-items: flex-end; }
|
|
.message-content {
|
|
max-width: 70%; padding: 10px 12px; border-radius: 14px; background: rgba(255,255,255,0.92); color: #0f172a;
|
|
border: 1px solid rgba(148,163,184,0.12); box-shadow: 0 8px 20px rgba(15, 23, 42, 0.05);
|
|
}
|
|
.message-self .message-content { background: linear-gradient(135deg, #4f46e5, #6366f1); color: #ffffff; }
|
|
.message-time { font-size: 12px; color: #94a3b8; margin-top: 5px; }
|
|
.input-area { padding: 20px 0 0; }
|
|
.toolbar { margin-top: 10px; display: flex; gap: 10px; flex-wrap: wrap; }
|
|
</style>
|
|
{% endblock %}
|