feat: full-field display for message/staff/accounts (raw_json + drawers)

Replicates the Customer all-fields pattern across the remaining three
entities. Backend now exposes `raw_json` on MessageOut / StaffOut /
AccountOut so the frontend can render every 云客 source field without a
second endpoint. Each entity gains a Chinese-label field utility + an
el-drawer surfacing all fields (structured-first, raw_json fallback,
unknown keys under 其他).

Backend:
- schemas.py: add `raw_json: dict | None` to MessageOut / StaffOut /
  AccountOut. MessageOut/StaffOut pick the column up via from_attributes;
  AccountOut is built explicitly in the router, which now threads
  `raw_json` through from both WechatAccount and WhatsappAccount rows.
- tests: new raw_json serialization tests for messages, staff, accounts
  (assert populated dict round-trips verbatim + NULL serializes as null).

Frontend (yunkes/):
- New utils: messageFields.ts, staffFields.ts, accountFields.ts — each
  with a 中文 label map, ordered groups, structured-column preference,
  and an 其他 fallback for unknown raw_json keys.
- Message: clicking a chat bubble opens an all-fields drawer (referMsg
  renders as pretty-printed JSON). Image preview + file download links
  stopPropagation so their own interactions still work.
- Staff: row click → drawer; columns relabeled (员工ID, 销售手机).
- Accounts: new AccountList.vue at /accounts with a 账号 nav item,
  unified WeChat/WhatsApp table (渠道/账号ID/昵称/微信号/归属员工/好友数/
  群数/收发/在职/最后更新), channel filter, pagination, row-click drawer.
- api: add Account type + getAccounts(); add raw_json to Message/Staff.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-01 17:47:50 +08:00
parent e5af1a2bc2
commit 6ac47a1203
16 changed files with 1562 additions and 13 deletions
+14 -5
View File
@@ -12,7 +12,14 @@ const props = defineProps<{ message: Message }>()
* `/api/media/{hash}` streaming endpoint exists for cases where the OSS URL is
* missing, but the current MessageOut schema exposes `file_url` only, so we
* degrade gracefully via `file_th_url || file_url` (forward-compatible).
*
* Clicking anywhere on the bubble (other than interactive children such as the
* image preview or the file download link) emits `open-detail` so the parent
* timeline can open the all-fields drawer for this message. The image preview
* and download link stop propagation so their own interactions still work.
*/
const emit = defineEmits<{ (e: 'open-detail', message: Message): void }>()
const isSend = computed(() => !!props.message.is_send)
// Centered (system/divider) rows ignore direction.
@@ -41,13 +48,15 @@ const imageThumbUrl = computed(
'yk-msg--center': isCentered,
}"
>
<!-- Centered: divider / system -->
<!-- Centered: divider / system — not clickable. -->
<div v-if="isCentered" class="yk-msg__divider">
{{ message.text || (message.type === 'divider' ? '—— 分割线 ——' : '系统消息') }}
</div>
<!-- Normal bubble row -->
<div v-else class="yk-msg__row">
<!-- Normal bubble row — click opens the all-fields drawer. The image
preview + file download link stop propagation so their own clicks
(zoom / navigate) don't also open the drawer. -->
<div v-else class="yk-msg__row yk-msg__row--clickable" @click="emit('open-detail', message)">
<div class="yk-msg__meta">
<span>{{ senderLabel }}</span>
<span v-if="timeLabel">{{ timeLabel }}</span>
@@ -59,7 +68,7 @@ const imageThumbUrl = computed(
</div>
<!-- image -->
<div v-else-if="message.type === 'image'">
<div v-else-if="message.type === 'image'" @click.stop>
<el-image
v-if="imageThumbUrl"
class="yk-msg__image"
@@ -98,7 +107,7 @@ const imageThumbUrl = computed(
</div>
<!-- file -->
<div v-else-if="message.type === 'file'">
<div v-else-if="message.type === 'file'" @click.stop>
<a
v-if="message.file_url"
class="yk-msg__chip yk-msg__chip--download"