u
This commit is contained in:
1 parent
2f8a815c62
commit
a09fdf0a66
127 files changed
+4518
-1159
No files matched your search
@@ -1,10 +1,15 @@
|
||||
<script setup>
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import Layout from '@/layout/index.vue'
|
||||
import { useWebSocket } from './stores/websocketStore'
|
||||
import { nextId } from './utils/uid'
|
||||
|
||||
const websocket = useWebSocket()
|
||||
|
||||
onMounted(() => {
|
||||
console.log(nextId())
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
websocket.disconnect()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { onUnmounted, watch } from 'vue'
|
||||
|
||||
export function useScrollWatcher({
|
||||
target, // 接收外部传入的 ref
|
||||
thresholdPercent = 0.05,
|
||||
delay = 0,
|
||||
onReachTop,
|
||||
onReachBottom,
|
||||
} = {}) {
|
||||
if (!target)
|
||||
throw new Error('target 必须传入一个 ref')
|
||||
|
||||
let lastScrollTop = 0
|
||||
let timeout = null
|
||||
const topTriggered = { value: false }
|
||||
const bottomTriggered = { value: false }
|
||||
|
||||
const handleScroll = (e) => {
|
||||
const el = e.target
|
||||
const scrollTop = el.scrollTop
|
||||
const scrollHeight = el.scrollHeight
|
||||
const clientHeight = el.clientHeight
|
||||
const scrollRange = scrollHeight - clientHeight
|
||||
if (scrollRange <= 0)
|
||||
return
|
||||
|
||||
const isScrollingUp = scrollTop < lastScrollTop
|
||||
const isScrollingDown = scrollTop > lastScrollTop
|
||||
lastScrollTop = scrollTop
|
||||
|
||||
const topThreshold = scrollRange * thresholdPercent
|
||||
const bottomThreshold = scrollRange * thresholdPercent
|
||||
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
if (isScrollingUp && scrollTop <= topThreshold && !topTriggered.value) {
|
||||
onReachTop?.()
|
||||
topTriggered.value = true
|
||||
}
|
||||
else if (scrollTop > topThreshold) {
|
||||
topTriggered.value = false
|
||||
}
|
||||
|
||||
if (isScrollingDown && scrollHeight - scrollTop - clientHeight <= bottomThreshold && !bottomTriggered.value) {
|
||||
onReachBottom?.()
|
||||
bottomTriggered.value = true
|
||||
}
|
||||
else if (scrollHeight - scrollTop - clientHeight > bottomThreshold) {
|
||||
bottomTriggered.value = false
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
|
||||
const bindScroll = (el) => {
|
||||
if (!el)
|
||||
return
|
||||
el.addEventListener('scroll', handleScroll)
|
||||
lastScrollTop = el.scrollTop
|
||||
}
|
||||
|
||||
const unbindScroll = (el) => {
|
||||
if (!el)
|
||||
return
|
||||
el.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
|
||||
// 监听 ref
|
||||
watch(target, (el, oldEl) => {
|
||||
if (oldEl)
|
||||
unbindScroll(oldEl)
|
||||
if (el)
|
||||
bindScroll(el)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (target.value)
|
||||
unbindScroll(target.value)
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
|
||||
const scrollToTop = () => {
|
||||
if (target.value)
|
||||
target.value.scrollTop = 0
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (target.value) {
|
||||
const el = target.value
|
||||
el.scrollTop = el.scrollHeight - el.clientHeight
|
||||
}
|
||||
}
|
||||
|
||||
return { scrollToTop, scrollToBottom }
|
||||
}
|
||||
@@ -190,6 +190,11 @@ export function useWebSocket() {
|
||||
function sendMessage(data) {
|
||||
if (isConnected.value && socket.value) {
|
||||
data.orgId = userStore.userInfo.orgid || ''
|
||||
|
||||
if (!data.timestamp) {
|
||||
data.timestamp = Date.now()
|
||||
}
|
||||
|
||||
socket.value.send(JSON.stringify(data))
|
||||
}
|
||||
}
|
||||
@@ -203,7 +208,7 @@ export function useWebSocket() {
|
||||
if (!isConnected.value)
|
||||
return
|
||||
sendMessage({
|
||||
type: 'login',
|
||||
type: 'online',
|
||||
from: userId,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
export function generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 格式化聊天时间
|
||||
* @param {number} timestamp - 毫秒级时间戳
|
||||
* @param {'list'|'chat'} [mode] - 显示模式:'list' 为列表,'chat' 为聊天记录
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatImTime(timestamp, mode = 'list') {
|
||||
if (!timestamp)
|
||||
return ''
|
||||
|
||||
const now = new Date()
|
||||
const date = new Date(timestamp)
|
||||
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
|
||||
// 去除时分秒的日期
|
||||
const nowMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const dateMidnight = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
const diffDays = Math.floor((nowMidnight - dateMidnight) / (1000 * 3600 * 24))
|
||||
|
||||
// 1️⃣ 当天
|
||||
if (diffDays === 0)
|
||||
return `${hour}:${minute}`
|
||||
|
||||
// 2️⃣ 昨天
|
||||
if (diffDays === 1)
|
||||
return `昨天 ${hour}:${minute}`
|
||||
|
||||
// 3️⃣ 一周内(周一到周日)
|
||||
const weekStart = new Date(nowMidnight.getTime() - ((now.getDay() || 7) - 1) * 24 * 3600 * 1000)
|
||||
if (dateMidnight >= weekStart) {
|
||||
return mode === 'chat'
|
||||
? `${weekDays[date.getDay()]} ${hour}:${minute}`
|
||||
: weekDays[date.getDay()]
|
||||
}
|
||||
|
||||
// 4️⃣ 本年或跨年(聊天记录模式:统一显示 YYYY/MM/DD HH:mm)
|
||||
const ymd = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
|
||||
|
||||
if (mode === 'chat')
|
||||
return `${ymd} ${hour}:${minute}`
|
||||
|
||||
// 列表模式区分本年/跨年
|
||||
if (date.getFullYear() === now.getFullYear())
|
||||
return `${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
|
||||
|
||||
return ymd
|
||||
}
|
||||
|
||||
export function formatTimestamp(timestamp) {
|
||||
if (!timestamp)
|
||||
return ''
|
||||
|
||||
// 如果是秒级时间戳(10位),转换成毫秒
|
||||
if (timestamp.toString().length === 10) {
|
||||
timestamp *= 1000
|
||||
}
|
||||
|
||||
const date = new Date(timestamp)
|
||||
const Y = date.getFullYear()
|
||||
const M = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const D = String(date.getDate()).padStart(2, '0')
|
||||
const h = String(date.getHours()).padStart(2, '0')
|
||||
const m = String(date.getMinutes()).padStart(2, '0')
|
||||
const s = String(date.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${Y}-${M}-${D} ${h}:${m}:${s}`
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 雪花 ID 生成器(字符串版,浏览器可用)
|
||||
* 结构:时间戳(ms)-机器ID-序列号
|
||||
*/
|
||||
class Genid {
|
||||
/**
|
||||
* @param {{
|
||||
* workerId: number, // 机器 ID
|
||||
* workerIdBitLength: number, // 机器 ID 位数
|
||||
* seqBitLength: number, // 序列号位数
|
||||
* baseTime: number // 起始时间戳
|
||||
* }}
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
if (options.workerId === undefined)
|
||||
throw new Error('WorkerId is required')
|
||||
|
||||
this.workerId = options.workerId
|
||||
this.workerIdBitLength = options.workerIdBitLength || 6
|
||||
this.seqBitLength = options.seqBitLength || 6
|
||||
this.baseTime = options.baseTime || 1577836800000 // 2020-01-01
|
||||
|
||||
this.maxSequence = (1 << this.seqBitLength) - 1
|
||||
this.sequence = 0
|
||||
this.lastTimestamp = 0
|
||||
}
|
||||
|
||||
// 获取当前时间戳
|
||||
currentTime() {
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
// 等待下一毫秒
|
||||
waitNextMillis(lastTimestamp) {
|
||||
let ts = this.currentTime()
|
||||
while (ts <= lastTimestamp) {
|
||||
ts = this.currentTime()
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
// 生成 ID(字符串)
|
||||
nextId() {
|
||||
let timestamp = this.currentTime()
|
||||
|
||||
if (timestamp < this.lastTimestamp) {
|
||||
throw new Error('Clock moved backwards')
|
||||
}
|
||||
|
||||
if (timestamp === this.lastTimestamp) {
|
||||
this.sequence++
|
||||
if (this.sequence > this.maxSequence) {
|
||||
timestamp = this.waitNextMillis(this.lastTimestamp)
|
||||
this.sequence = 0
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.sequence = 0
|
||||
}
|
||||
|
||||
this.lastTimestamp = timestamp
|
||||
|
||||
const timePart = (timestamp - this.baseTime).toString().padStart(13, '0') // 时间戳部分
|
||||
const workerPart = this.workerId.toString().padStart(this.workerIdBitLength, '0')
|
||||
const seqPart = this.sequence.toString().padStart(this.seqBitLength, '0')
|
||||
|
||||
return timePart + workerPart + seqPart // 返回字符串
|
||||
}
|
||||
|
||||
// 批量生成
|
||||
nextIds(count) {
|
||||
const ids = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
ids.push(this.nextId())
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// 解析 ID
|
||||
parseId(id) {
|
||||
const timePart = id.slice(0, 13)
|
||||
const workerPart = id.slice(13, 13 + this.workerIdBitLength)
|
||||
const seqPart = id.slice(13 + this.workerIdBitLength)
|
||||
|
||||
return {
|
||||
timestamp: Number.parseInt(timePart) + this.baseTime,
|
||||
workerId: Number.parseInt(workerPart),
|
||||
sequence: Number.parseInt(seqPart),
|
||||
date: new Date(Number.parseInt(timePart) + this.baseTime),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// 单例模式
|
||||
// -----------------------
|
||||
const defaultGenid = new Genid({ workerId: 1 })
|
||||
|
||||
// 工厂函数
|
||||
function createGenid(options) {
|
||||
return new Genid(options)
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function nextId() {
|
||||
return defaultGenid.nextId()
|
||||
}
|
||||
|
||||
function parseId(id) {
|
||||
return defaultGenid.parseId(id)
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// 导出
|
||||
// -----------------------
|
||||
export { createGenid, Genid, nextId, parseId }
|
||||
@@ -1,24 +1,55 @@
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { axiosGet, axiosPost } from '@/utils/axios'
|
||||
import { useWebSocket } from '../../stores/websocketStore'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const websocket = useWebSocket()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const tableData = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axiosGet('/loadCompanyList')
|
||||
tableData.value = res.data
|
||||
const res = await axiosGet('/selectlistbysql', {
|
||||
sql: `select b_id,b_name from b_user where b_canuse = '1' `,
|
||||
})
|
||||
tableData.value = res.data.records
|
||||
}
|
||||
catch (err) {
|
||||
ElMessage.error(`请求错误:${err.response?.data?.message || err.message}`)
|
||||
}
|
||||
})
|
||||
async function toIm(itemData) {
|
||||
const { userid: userId } = userStore.userInfo || {}
|
||||
const friendId = itemData?.b_id
|
||||
|
||||
if (!userId || !friendId) {
|
||||
ElMessage.warning('用户信息或好友ID不存在')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axiosPost('/ensureFriend', { userId, friendId })
|
||||
|
||||
if (res.success) {
|
||||
router.push({
|
||||
path: '/im',
|
||||
query: { friendId },
|
||||
})
|
||||
}
|
||||
else {
|
||||
console.error('ensureFriend 请求错误:', res.message)
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('ensureFriend 请求错误:', err)
|
||||
ElMessage.error(err.response?.data?.message || err.message || '请求失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -34,7 +65,7 @@ onMounted(async () => {
|
||||
|
||||
<el-table-column fixed="right" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button v-if="userStore.userInfo.userid !== scope.row.b_id" link type="primary" size="small">
|
||||
<el-button v-if="userStore.userInfo.userid !== scope.row.b_id" link type="primary" size="small" @click="toIm(scope.row)">
|
||||
聊天
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,125 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>列表示例</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f6fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.list {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e6e9ef;
|
||||
}
|
||||
|
||||
.list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-item__main {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.list-item__time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.list-item__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-item__extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tag-success {
|
||||
background-color: #2ecc71;
|
||||
}
|
||||
|
||||
.tag-secondary {
|
||||
background-color: #95a5a6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>布局 1:时间在主信息下面</h2>
|
||||
<div class="list">
|
||||
<div class="list-item">
|
||||
<div class="list-item__info">
|
||||
<div class="list-item__main">箱号:123456</div>
|
||||
<div class="list-item__time">完成时间:2025-10-11 14:30</div>
|
||||
</div>
|
||||
<div class="list-item__extra">
|
||||
<span class="tag tag-success">已完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>布局 2:时间在右侧 extra 区</h2>
|
||||
<div class="list">
|
||||
<div class="list-item">
|
||||
<div class="list-item__main">箱号:123457</div>
|
||||
<div class="list-item__extra">
|
||||
<div class="list-item__time">2025-10-11 15:00</div>
|
||||
<span class="tag tag-secondary">未完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>布局 3:时间与状态在同一行,右侧对齐</h2>
|
||||
<div class="list">
|
||||
<div class="list-item">
|
||||
<div class="list-item__main">箱号:123458</div>
|
||||
<div class="list-item__extra">
|
||||
<span class="tag tag-success">已完成</span>
|
||||
<div class="list-item__time">2025-10-11 16:00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>布局 4:多行布局,箱号 + 时间在左,状态在右</h2>
|
||||
<div class="list">
|
||||
<div class="list-item">
|
||||
<div class="list-item__info">
|
||||
<div class="list-item__main">箱号:123459</div>
|
||||
<div class="list-item__time">完成时间:2025-10-11 17:30</div>
|
||||
</div>
|
||||
<div class="list-item__extra">
|
||||
<span class="tag tag-secondary">未完成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
function Snowflake() {
|
||||
this.epoch = 1698796800000 // 自定义起始时间(毫秒)
|
||||
this.machineId = 1 // 固定 machineId
|
||||
this.sequence = 0
|
||||
this.lastTimestamp = -1
|
||||
|
||||
this.sequenceBits = 12
|
||||
this.machineBits = 10
|
||||
this.maxSequence = 2 ** this.sequenceBits - 1 // 4095
|
||||
}
|
||||
|
||||
// 获取当前时间(毫秒)
|
||||
Snowflake.prototype.currentTime = function () {
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
// 等待下一毫秒
|
||||
Snowflake.prototype.waitNextMillisecond = function (lastTimestamp) {
|
||||
let timestamp = this.currentTime()
|
||||
while (timestamp <= lastTimestamp) {
|
||||
timestamp = this.currentTime()
|
||||
}
|
||||
return timestamp
|
||||
}
|
||||
|
||||
// 生成 BigInt ID
|
||||
Snowflake.prototype.nextId = function () {
|
||||
let timestamp = this.currentTime()
|
||||
|
||||
if (timestamp < this.lastTimestamp) {
|
||||
throw new Error('Clock moved backwards. Refusing to generate id.')
|
||||
}
|
||||
|
||||
if (timestamp === this.lastTimestamp) {
|
||||
this.sequence = (this.sequence + 1) & this.maxSequence
|
||||
if (this.sequence === 0) {
|
||||
timestamp = this.waitNextMillisecond(this.lastTimestamp)
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.sequence = 0
|
||||
}
|
||||
|
||||
this.lastTimestamp = timestamp
|
||||
|
||||
const id
|
||||
= (BigInt(timestamp - this.epoch) << BigInt(this.machineBits + this.sequenceBits))
|
||||
| (BigInt(this.machineId) << BigInt(this.sequenceBits))
|
||||
| BigInt(this.sequence)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
// 返回固定长度字符串 ID(适合数据库存储)
|
||||
Snowflake.prototype.nextIdStr = function (padLength = 20) {
|
||||
return this.nextId().toString().padStart(padLength, '0')
|
||||
}
|
||||
Reference in new issue
Block a user