| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623 |
- <template>
- <div class="app-container seat-config-container">
- <!-- 主界面:坐席配置列表 -->
- <template v-if="!showTicketSystem">
- <el-card shadow="never" class="table-header">
- <div class="flex-between">
- <div class="search-box">
- <el-input
- v-model="queryParams.seatName"
- placeholder="请输入坐席名称"
- style="width: 240px"
- clearable
- @keyup.enter="handleQuery"
- >
- <template #append>
- <el-button :icon="Search" @click="handleQuery" />
- </template>
- </el-input>
- </div>
- <div class="btn-group">
- <el-button type="primary" :icon="Plus" @click="handleAdd">创建坐席</el-button>
- <el-button type="success" :icon="Tickets" @click="showTicketSystem = true">客服工单</el-button>
- </div>
- </div>
- </el-card>
- <el-card shadow="never" class="mt-20">
- <el-table :data="seatList" v-loading="loading" style="width: 100%">
- <el-table-column type="selection" width="55" align="center" />
- <el-table-column label="坐席名称" align="center" prop="seatName" />
- <el-table-column label="坐席头像" align="center" width="100">
- <template #default="scope">
- <el-avatar :size="40" :src="scope.row.avatar" />
- </template>
- </el-table-column>
- <el-table-column label="负责模块" align="center" prop="module">
- <template #default="scope">
- <dict-tag :options="main_agent_module" :value="scope.row.module" />
- </template>
- </el-table-column>
- <el-table-column label="客服姓名" align="left" min-width="200">
- <template #default="scope">
- <div class="waiter-tag-wrap">
- <span v-for="(waiter, index) in scope.row.waiters" :key="index" class="waiter-name">
- {{ waiter.nickName || waiter.userName }} ({{ waiter.userId }}){{ index < scope.row.waiters.length - 1 ? '、' : '' }}
- </span>
- </div>
- </template>
- </el-table-column>
- <el-table-column label="状态" align="center" prop="status">
- <template #default="scope">
- <el-switch
- v-model="scope.row.status"
- :active-value="1"
- :inactive-value="0"
- @change="handleStatusChange(scope.row)"
- />
- </template>
- </el-table-column>
- <el-table-column label="操作" align="center" width="150">
- <template #default="scope">
- <el-button link type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
- <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </el-card>
- </template>
- <!-- 子界面:客服工单管理系统 (根据图2重构) -->
- <div v-else class="ticket-system-container animate__animated animate__fadeIn">
- <div class="ticket-header">
- <div class="title-row">
- <div class="back-btn" @click="showTicketSystem = false">
- <el-icon><ArrowLeft /></el-icon> 返回坐席配置
- </div>
- <h2 class="sys-title">客服工单中心</h2>
- </div>
- <el-tabs v-model="activeTicketTab" class="ticket-tabs">
- <el-tab-pane label="全部状态" name="all" />
- <el-tab-pane label="待处理" name="pending" />
- <el-tab-pane label="处理中" name="processing" />
- <el-tab-pane label="已完成" name="completed" />
- <el-tab-pane label="已废弃" name="abandoned" />
- </el-tabs>
- </div>
- <el-card shadow="never" class="filter-card mt-20">
- <el-form :inline="true" :model="ticketQuery" class="demo-form-inline">
- <el-form-item label="反馈ID">
- <el-input v-model="ticketQuery.id" placeholder="如:20991216..." clearable />
- </el-form-item>
- <el-form-item label="用户ID">
- <el-input v-model="ticketQuery.userId" placeholder="搜索用户ID" clearable />
- </el-form-item>
- <el-form-item label="反馈渠道">
- <el-select v-model="ticketQuery.source" placeholder="全部渠道" style="width: 140px" clearable>
- <el-option label="小程序" value="小程序" />
- <el-option label="商家" value="商家" />
- </el-select>
- </el-form-item>
- <el-form-item label="问题分类">
- <el-select v-model="ticketQuery.category" placeholder="全部分类" style="width: 140px" clearable>
- <el-option label="这是问题分类1" value="这是问题分类1" />
- <el-option label="测评异常" value="测评异常" />
- <el-option label="功能咨询" value="功能咨询" />
- <el-option label="支付问题" value="支付问题" />
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" :icon="Search" @click="handleTicketSearch">查询</el-button>
- <el-button @click="resetTicketQuery">重置</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- <el-card shadow="never" class="mt-20">
- <el-table :data="displayTicketList" style="width: 100%" v-loading="ticketLoading">
- <el-table-column prop="id" label="反馈ID" width="140" />
- <el-table-column label="用户信息" width="160">
- <template #default="scope">
- <div class="u-info">
- <span class="u-name">{{ scope.row.userName }}</span>
- <span class="u-id">ID:{{ scope.row.userId }}</span>
- </div>
- </template>
- </el-table-column>
- <el-table-column prop="content" label="反馈内容" min-width="250" show-overflow-tooltip />
- <el-table-column label="状态" width="100" align="center">
- <template #default="scope">
- <el-tag :type="getStatusType(scope.row.status)" effect="dark" size="small">
- {{ getStatusLabel(scope.row.status) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="source" label="反馈渠道" width="120" align="center" />
- <el-table-column prop="category" label="问题分类" width="140" align="center" />
- <el-table-column prop="createTime" label="创建时间" width="180" align="center" />
- <el-table-column prop="finishTime" label="完结时间" width="180" align="center">
- <template #default="scope">{{ scope.row.finishTime || '-' }}</template>
- </el-table-column>
- <el-table-column label="操作" width="120" fixed="right" align="center">
- <template #default="scope">
- <el-button
- v-if="scope.row.status === 'pending' || scope.row.status === 'processing'"
- link
- type="primary"
- @click="handleTicketDetail(scope.row)"
- >处理</el-button>
- <el-button
- link
- type="info"
- @click="handleTicketDetail(scope.row)"
- >详情</el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination-wrap">
- <el-pagination background layout="prev, pager, next, total" :total="displayTicketList.length" />
- </div>
- </el-card>
- </div>
- <!-- 工单详情与处理对话框 -->
- <el-dialog
- v-model="ticketDetailOpen"
- title="工单业务处理"
- width="1100px"
- custom-class="ticket-detail-dialog"
- append-to-body
- >
- <div class="ticket-detail-wrapper">
- <!-- 左侧工单快照 -->
- <div class="ticket-snapshot">
- <div class="snap-title">工单基础信息</div>
- <el-descriptions :column="1" border>
- <el-descriptions-item label="反馈ID">{{ currentTicket?.id }}</el-descriptions-item>
- <el-descriptions-item label="反馈状态">
- <el-tag size="small">{{ getStatusLabel(currentTicket?.status) }}</el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="反馈人">{{ currentTicket?.userName }} ({{ currentTicket?.userId }})</el-descriptions-item>
- <el-descriptions-item label="渠道">{{ currentTicket?.source }}</el-descriptions-item>
- <el-descriptions-item label="原始反馈内容">
- <div class="raw-content">{{ currentTicket?.content }}</div>
- </el-descriptions-item>
- </el-descriptions>
- <div class="snap-title mt-20">处理操作</div>
- <el-form label-position="top">
- <el-form-item label="流转状态">
- <el-radio-group v-model="ticketHandleForm.status" size="small">
- <el-radio-button label="processing">处理中</el-radio-button>
- <el-radio-button label="completed">已完成</el-radio-button>
- <el-radio-button label="abandoned">废弃</el-radio-button>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="快捷回复">
- <el-input
- type="textarea"
- :rows="4"
- v-model="ticketHandleForm.reply"
- placeholder="请输入处理意见或回复用户的内容..."
- />
- </el-form-item>
- <el-button type="primary" class="full-width" @click="submitTicketHandle">提交处理结果</el-button>
- </el-form>
- </div>
- <!-- 右侧对话追踪流 (模仿图2下方效果) -->
- <div class="ticket-chat-tracker">
- <div class="tracker-header">沟通过程溯源</div>
- <div class="chat-flow-container">
- <div
- v-for="(chat, index) in ticketChats"
- :key="index"
- :class="['chat-bubble-row', chat.sender === 'user' ? 'user' : 'waiter']"
- >
- <el-avatar :size="40" :src="chat.avatar" />
- <div class="chat-content-box">
- <div class="chat-meta">{{ chat.name }} · {{ chat.time }}</div>
- <div class="chat-bubble">{{ chat.content }}</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </el-dialog>
- <!-- 添加或修改坐席对话框 (原有逻辑) -->
- <el-dialog :title="title" v-model="open" width="500px" append-to-body>
- <el-form ref="seatRef" :model="form" :rules="rules" label-width="100px">
- <el-form-item label="坐席名称" prop="seatName">
- <el-input v-model="form.seatName" placeholder="请输入坐席名称" />
- </el-form-item>
- <el-form-item label="坐席头像" prop="avatar">
- <div class="avatar-uploader-box" @click="triggerAvatarUpload" :class="{'has-image': !!avatarPreviewUrl}">
- <template v-if="avatarPreviewUrl">
- <el-image :src="avatarPreviewUrl" class="avatar-preview" fit="cover" />
- <div class="avatar-hover-mask">
- <el-icon><Picture /></el-icon>
- <span>点击更换头像</span>
- </div>
- </template>
- <div v-else class="avatar-placeholder">
- <el-icon><Plus /></el-icon>
- <div class="placeholder-text">添加头像</div>
- </div>
- <input type="file" ref="avatarInput" style="display: none" accept="image/*" @change="handleAvatarUpload" />
- </div>
- </el-form-item>
- <el-form-item label="启用状态" prop="status">
- <el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
- </el-form-item>
- <el-form-item label="选择客服" prop="waiterIds">
- <el-select v-model="form.waiterIds" multiple placeholder="请选择客服" style="width: 100%">
- <el-option v-for="item in waiterOptions" :key="item.userId" :label="item.nickName" :value="item.userId" />
- </el-select>
- </el-form-item>
- <el-form-item label="负责模块" prop="module">
- <el-select v-model="form.module" placeholder="请选择负责模块" style="width: 100%">
- <el-option
- v-for="dict in main_agent_module"
- :key="dict.value"
- :label="dict.label"
- :value="dict.value"
- />
- </el-select>
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="open = false">取 消</el-button>
- <el-button type="primary" @click="submitForm">确 定</el-button>
- </div>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup name="SeatConfig">
- import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue';
- import { Search, Plus, Tickets, ArrowLeft, UserFilled, Picture } from '@element-plus/icons-vue';
- import { ElMessage, ElMessageBox } from 'element-plus';
- //import { getSeatList, getSeatDetail, addSeat, updateSeat, deleteSeat, updateSeatStatus, uploadSeatAvatar } from '@/api/chat/seat';
- import { getSeatList, getSeatDetail, addSeat, updateSeat, deleteSeat, updateSeatStatus } from '@/api/chat/seat';
- import { listUser } from '@/api/system/user';
- const { proxy } = getCurrentInstance();
- const { main_agent_module } = proxy.useDict("main_agent_module");
- // --- 坐席配置逻辑 ---
- const loading = ref(false);
- const total = ref(0);
- const seatList = ref([]);
- const title = ref("");
- const open = ref(false);
- const waiterOptions = ref([]); // 这里存放所有可选的系统用户
- const queryParams = reactive({
- pageNum: 1,
- pageSize: 10,
- seatName: undefined,
- module: undefined
- });
- const form = ref({});
- const seatRef = ref(null);
- const avatarPreviewUrl = ref("");
- const rules = {
- seatName: [{ required: true, message: "坐席名称不能为空", trigger: "blur" }],
- module: [{ required: true, message: "负责模块不能为空", trigger: "change" }]
- };
- const avatarInput = ref(null);
- function triggerAvatarUpload() { avatarInput.value.click(); }
- async function handleAvatarUpload(e) {
- const file = e.target.files[0];
- if (!file) return;
- try {
- const res = await uploadSeatAvatar(file);
- if (res.code === 200) {
- // 核心修改 1:存储 ossId 到 avatar 字段,用于提交后端
- form.value.avatar = res.data.ossId;
- // 核心修改 2:存储 url 用于前端预览
- avatarPreviewUrl.value = res.data.url;
- ElMessage.success('头像上传成功');
- }
- } catch (error) {
- ElMessage.error('头像上传失败');
- }
- }
- async function getList() {
- loading.value = true;
- try {
- const res = await getSeatList(queryParams);
- if (res.code === 200 || res.code === 0) {
- // 确保后端返回的是 TableDataInfo 格式
- seatList.value = res.rows || [];
- total.value = res.total || 0;
- }
- } catch (error) {
- console.error('获取坐席列表失败:', error);
- } finally {
- loading.value = false;
- }
- }
- /** 查询客服用户列表 */
- async function getWaiterOptions() {
- try {
- // 假设客服用户在某个特定的部门或角色下,这里简单获取所有活动用户
- const res = await listUser({ status: '0', pageNum: 1, pageSize: 1000 });
- if (res.code === 200) {
- waiterOptions.value = res.rows;
- }
- } catch (error) {
- console.error('获取客服列表失败:', error);
- }
- }
- function handleAdd() {
- reset();
- title.value = "新增坐席";
- open.value = true;
- }
- async function handleUpdate(row) {
- reset();
- try {
- const res = await getSeatDetail(row.id);
- if (res.code === 200 || res.code === 0) {
- form.value = res.data;
- // 设置头像预览(如果后端返回的是URL)
- avatarPreviewUrl.value = res.data.avatar;
- // 转换关联的客服ID列表供下拉框显示
- if (res.data.waiters) {
- form.value.waiterIds = res.data.waiters.map(w => w.userId);
- }
- title.value = "编辑坐席";
- open.value = true;
- }
- } catch (error) {
- console.error('获取坐席详情失败:', error);
- }
- }
- function reset() {
- form.value = { id: undefined, seatName: undefined, avatar: undefined, status: 1, waiterIds: [], module: undefined };
- avatarPreviewUrl.value = "";
- }
- async function submitForm() {
- seatRef.value.validate(async valid => {
- if (valid) {
- try {
- const data = { ...form.value };
- if (data.id) {
- await updateSeat(data.id, data);
- ElMessage.success('修改成功');
- } else {
- await addSeat(data);
- ElMessage.success('新增成功');
- }
- open.value = false;
- getList();
- } catch (error) {
- console.error('保存坐席失败:', error);
- }
- }
- });
- }
- function handleDelete(row) {
- ElMessageBox.confirm('确认要删除该坐席吗?', '警告', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- }).then(async () => {
- try {
- await deleteSeat(row.id);
- ElMessage.success('删除成功');
- getList();
- } catch (error) {
- console.error('删除坐席失败:', error);
- }
- }).catch(() => {});
- }
- async function handleStatusChange(row) {
- try {
- await updateSeatStatus(row.id, row.status);
- ElMessage.success(row.status === 1 ? '已启用' : '已停用');
- } catch (error) {
- // 恢复状态
- row.status = row.status === 1 ? 0 : 1;
- console.error('修改状态失败:', error);
- }
- }
- function handleQuery() {
- queryParams.pageNum = 1;
- getList();
- }
- onMounted(() => {
- getList();
- getWaiterOptions();
- });
- // --- 新增:客服工单子系统逻辑 ---
- const showTicketSystem = ref(false);
- const activeTicketTab = ref('all');
- const ticketQuery = reactive({ id: '', userId: '', source: '', category: '' });
- const ticketDetailOpen = ref(false);
- const currentTicket = ref(null);
- const ticketHandleForm = reactive({ status: 'processing', reply: '' });
- const ticketLoading = ref(false);
- const ticketList = ref([
- { id: '20991216194', userId: 'ID19451612', userName: '用户张三', content: '这是一段反馈内容,关于这个反馈工单的文字内容,这是反馈内容,反馈文字。', status: 'pending', source: '小程序', category: '这是问题分类1', createTime: '2099-10-22 17:07:25', finishTime: null },
- { id: '20991216193', userId: 'ID11679458', userName: '用户李四', content: '反馈内容展示,这里是一段反馈的具体描述,系统报错无法点击。', status: 'processing', source: '商家', category: '测评异常', createTime: '2099-10-22 16:30:10', finishTime: null },
- { id: '20991216192', userId: 'ID29416751', userName: '用户王五', content: '咨询关于审计师报名的相关流程。', status: 'completed', source: '小程序', category: '功能咨询', createTime: '2099-10-22 11:15:25', finishTime: '2099-10-24 14:25:07' },
- { id: '20991216191', userId: 'ID19167453', userName: '用户六六', content: '我在支付时遇到了问题,钱扣了但是订单没显示。', status: 'abandoned', source: '商家', category: '支付问题', createTime: '2099-10-22 09:12:25', finishTime: '2099-10-22 10:25:07' },
- ]);
- const displayTicketList = computed(() => {
- return ticketList.value.filter(item => {
- // 状态筛选
- if (activeTicketTab.value !== 'all' && item.status !== activeTicketTab.value) return false;
- // 关键字筛选
- if (ticketQuery.id && !item.id.includes(ticketQuery.id)) return false;
- if (ticketQuery.userId && !item.userId.includes(ticketQuery.userId)) return false;
- if (ticketQuery.source && item.source !== ticketQuery.source) return false;
- if (ticketQuery.category && item.category !== ticketQuery.category) return false;
- return true;
- });
- });
- const handleTicketSearch = () => {
- ticketLoading.value = true;
- setTimeout(() => {
- ticketLoading.value = false;
- ElMessage.success('查询成功');
- }, 300);
- };
- const ticketChats = ref([
- { sender: 'user', name: '南风未起', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix', content: '你好,我刚咨询的岗位有点问题', time: '18:27:11' },
- { sender: 'waiter', name: '系统客服-小张', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Waiter', content: '您好,非常抱歉让您遇到了问题!请问具体是什么情况呢?', time: '18:28:27' },
- { sender: 'user', name: '南风未起', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix', content: '看不到岗位信息,无法进行测评', time: '18:28:52' },
- ]);
- function getStatusType(status) {
- const map = { pending: 'danger', processing: 'primary', completed: 'success', abandoned: 'info' };
- return map[status] || 'info';
- }
- function getStatusLabel(status) {
- const map = { pending: '待处理', processing: '处理中', completed: '已完成', abandoned: '已废弃' };
- return map[status] || '未知';
- }
- function handleTicketDetail(row) {
- currentTicket.value = row;
- ticketDetailOpen.value = true;
- ticketHandleForm.status = row.status === 'pending' ? 'processing' : row.status;
- ticketHandleForm.reply = '';
- }
- function resetTicketQuery() {
- Object.keys(ticketQuery).forEach(key => ticketQuery[key] = '');
- handleTicketSearch();
- }
- function submitTicketHandle() {
- ElMessage.success('工单处理意见已提交');
- ticketDetailOpen.value = false;
- // 此处应更新列表状态
- }
- </script>
- <style scoped lang="scss">
- .seat-config-container {
- background-color: #f8fafc;
- min-height: calc(100vh - 40px);
- padding: 24px;
- box-sizing: border-box;
- .flex-between { display: flex; justify-content: space-between; align-items: center; }
- .mt-10 { margin-top: 10px; }
- .mt-20 { margin-top: 20px; }
- .table-header { border: none; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.04); }
- /* 坐席列表特有 */
- .waiter-tag-wrap { color: #64748b; font-size: 13px; .waiter-name { display: inline-block; margin-bottom: 2px; } }
-
- .mb-10 { margin-bottom: 10px; }
- /* 工单系统样式 (现代化重构) */
- .ticket-system-container {
- .ticket-header {
- background: #fff; padding: 24px 24px 0; border-radius: 12px 12px 0 0; box-shadow: 0 2px 10px rgba(0,0,0,0.02);
- .title-row {
- display: flex; align-items: center; gap: 20px; margin-bottom: 20px;
- .back-btn { display: flex; align-items: center; gap: 6px; font-size: 14px; color: #64748b; cursor: pointer; &:hover { color: #2563eb; } }
- .sys-title { margin: 0; font-size: 20px; font-weight: 800; color: #1e293b; }
- }
- .ticket-tabs { :deep(.el-tabs__item) { font-size: 15px; font-weight: 600; padding: 0 25px; } }
- }
- .filter-card { border: none; border-radius: 0 0 12px 12px; }
- .u-info { display: flex; flex-direction: column; .u-name { font-weight: 700; color: #1e293b; } .u-id { font-size: 11px; color: #94a3b8; margin-top: 2px; } }
- .pagination-wrap { display: flex; justify-content: flex-end; padding: 20px 0 0; }
- }
- /* 通用 */
- :deep(.el-card) { border: none; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.04); }
- }
- /* 独立样式:兼容 append-to-body 弹窗中的上传组件 */
- .avatar-uploader-box {
- width: 120px; height: 120px; border: 2px dashed #b0b8c1; border-radius: 12px;
- background: #fdfdfd; position: relative; cursor: pointer; overflow: hidden;
- display: flex; flex-direction: column; align-items: center; justify-content: center;
- transition: all 0.3s ease;
- margin-top: 5px;
-
- &:hover {
- border-color: #2563eb;
- background: #eff6ff;
- box-shadow: 0 8px 24px rgba(37, 99, 235, 0.1);
- transform: translateY(-2px);
- .avatar-hover-mask { opacity: 1; }
- }
-
- &.has-image { border-style: solid; border-color: #f1f5f9; }
- .avatar-preview { width: 100%; height: 100%; object-fit: cover;}
-
- .avatar-placeholder {
- display: flex; flex-direction: column; align-items: center; gap: 8px;
- color: #64748b;
- .el-icon { font-size: 32px; font-weight: 300; }
- .placeholder-text { font-size: 13px; font-weight: 600; }
- }
- .avatar-hover-mask {
- position: absolute; inset: 0; background: rgba(15, 23, 42, 0.6); color: #fff;
- display: flex; flex-direction: column; align-items: center; justify-content: center;
- gap: 6px; font-size: 11px; opacity: 0; transition: 0.2s;
- backdrop-filter: blur(2px);
- .el-icon { font-size: 22px; }
- }
- }
- /* 工单详情对话框样式 */
- .ticket-detail-wrapper {
- display: flex; gap: 24px; height: 600px;
-
- .ticket-snapshot {
- flex: 1; border-right: 1px solid #f1f5f9; padding-right: 24px; overflow-y: auto;
- .snap-title { font-size: 16px; font-weight: 800; color: #1e293b; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; &::before { content: ''; width: 4px; height: 16px; background: #2563eb; border-radius: 2px; margin-right: 10px; } }
- .raw-content { background: #f8fafc; padding: 12px; border-radius: 8px; font-size: 13px; color: #475569; line-height: 1.6; }
- .full-width { width: 100%; margin-top: 20px; font-weight: 700; height: 40px; }
- }
- .ticket-chat-tracker {
- flex: 1.5; display: flex; flex-direction: column;
- .tracker-header { font-size: 16px; font-weight: 800; color: #1e293b; margin-bottom: 20px; }
- .chat-flow-container {
- flex: 1; background: #f9fafb; border-radius: 12px; padding: 24px; overflow-y: auto; display: flex; flex-direction: column; gap: 20px;
- .chat-bubble-row {
- display: flex; gap: 12px;
- &.user { flex-direction: row; .chat-content-box { .chat-bubble { background: #fff; border: 1px solid #eef2f6; border-radius: 2px 16px 16px 16px; } } }
- &.waiter { flex-direction: row-reverse; .chat-content-box { align-items: flex-end; .chat-meta { text-align: right; } .chat-bubble { background: #ecfdf5; border: 1px solid #d1fae5; color: #065f46; border-radius: 16px 2px 16px 16px; } } }
- .chat-content-box {
- display: flex; flex-direction: column; max-width: 80%;
- .chat-meta { font-size: 12px; color: #94a3b8; margin-bottom: 6px; }
- .chat-bubble { padding: 12px 16px; font-size: 14px; line-height: 1.6; box-shadow: 0 4px 6px rgba(0,0,0,0.02); }
- }
- }
- }
- }
- }
- .animate__fadeIn { animation: fadeIn 0.4s; }
- @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
- </style>
|