refactor: 四端完成 Options API → script setup + vuex → pinia 迁移

- buyer(95) / seller(169) / im(53) / manager(280) 共 597 个组件全部从 Options API 转换为 <script setup>
- 四端 vuex 全部移除,状态管理统一迁移至 pinia 4(stores/ 目录)
- util.js 等基础设施去除 vm 首参依赖;全局属性($Message/$filters 等)在 script 内改为具名导入
- 被外部 ref 访问的公共组件均已补齐 defineExpose(清单见各端 MIGRATION-VUE3.md)
- 各端 eslint 启用 vue/component-api-style 防止新代码回退 Options API
- 四端 yarn build 全部通过;迁移约定与注意事项已更新至各端 MIGRATION-VUE3.md
This commit is contained in:
lifenlong
2026-10-13 07:06:59 +08:00
parent 870bacdd8c
commit 88fc1b0b2a
674 changed files with 71220 additions and 69943 deletions

View File

@@ -27,7 +27,18 @@
</div>
</template>
<script>
<script setup>
import { computed, ref } from "vue";
defineOptions({ name: 'AudioMessage' });
defineProps({
src: {
type: String,
default: '',
},
});
function formatSeconds(value) {
var theTime = parseInt(value) // 秒
var theTime1 = 0 // 分
@@ -59,64 +70,47 @@ function formatSeconds(value) {
return result
}
export default {
name: 'AudioMessage',
props: {
src: {
type: String,
default: '',
},
},
data() {
return {
loading: true,
isPlay: false,
duration: 0,
currentTime: 0,
progress: 0,
}
},
computed: {
getTotalDuration() {
return formatSeconds(this.duration)
},
getCurrDuration() {
return formatSeconds(this.currentTime)
},
},
methods: {
toPlay() {
if (this.loading) {
return
}
const loading = ref(true)
const isPlay = ref(false)
const duration = ref(0)
const currentTime = ref(0)
const progress = ref(0)
let audio = this.$refs.audio
if (this.isPlay) {
audio.pause()
} else {
audio.play()
}
this.isPlay = !this.isPlay
},
const audio = ref(null)
// 当目前的播放位置已更改时
timeupdate() {
let audio = this.$refs.audio
this.currentTime = audio.currentTime
this.progress = (audio.currentTime / audio.duration) * 100
},
const getTotalDuration = computed(() => formatSeconds(duration.value))
const getCurrDuration = computed(() => formatSeconds(currentTime.value))
// 当浏览器可以播放音频/视频时
canplay() {
this.duration = this.$refs.audio.duration
this.loading = false
},
function toPlay() {
if (loading.value) {
return
}
// 当目前的播放列表已结束时
ended() {
this.isPlay = false
},
},
let audioEl = audio.value
if (isPlay.value) {
audioEl.pause()
} else {
audioEl.play()
}
isPlay.value = !isPlay.value
}
// 当目前的播放位置已更改时
function timeupdate() {
let audioEl = audio.value
currentTime.value = audioEl.currentTime
progress.value = (audioEl.currentTime / audioEl.duration) * 100
}
// 当浏览器可以播放音频/视频时
function canplay() {
duration.value = audio.value.duration
loading.value = false
}
// 当目前的播放列表已结束时
function ended() {
isPlay.value = false
}
</script>
<style scoped lang="less">

View File

@@ -20,44 +20,40 @@
<pre class="lum-scrollbar" v-html="formatCode(code, lang)" />
</div>
</template>
<script>
<script setup>
import { ref } from 'vue'
import Prism from 'prismjs'
import 'prismjs/themes/prism-okaidia.css'
export default {
name: 'CodeMessage',
props: {
code: {
type: [String, Number],
default: '',
},
lang: {
type: String,
default: '',
},
maxwidth: {
type: Boolean,
default: false,
},
defineOptions({ name: 'CodeMessage' })
const props = defineProps({
code: {
type: [String, Number],
default: '',
},
data() {
return {
fullscreen: false,
lineNumber: 0,
}
lang: {
type: String,
default: '',
},
created() {
this.lineNumber = this.code.split(/\n/).length
},
methods: {
formatCode(code, lang) {
try {
return Prism.highlight(code, Prism.languages[lang], lang) + '<br/>'
} catch (error) {
return code
}
},
maxwidth: {
type: Boolean,
default: false,
},
})
const fullscreen = ref(false)
const lineNumber = ref(0)
// created
lineNumber.value = props.code.split(/\n/).length
function formatCode(code, lang) {
try {
return Prism.highlight(code, Prism.languages[lang], lang) + '<br/>'
} catch (error) {
return code
}
}
</script>
<style lang="less" scoped>

View File

@@ -16,39 +16,36 @@
</div>
</div>
</template>
<script>
<script setup>
import { ref } from 'vue'
import { formatSize, download } from '@/utils/functions'
export default {
name: 'FileMessage',
props: {
file: {
type: Object,
required: true,
},
record_id: {
type: Number,
required: true,
default: 0,
},
defineOptions({ name: 'FileMessage' })
const props = defineProps({
file: {
type: Object,
required: true,
},
data() {
return {
file_id: 0,
ext: '',
fileName: '',
fileSize: '',
}
record_id: {
type: Number,
required: true,
default: 0,
},
created() {
this.file_id = this.file.id
this.ext = this.file.file_suffix.toUpperCase()
this.fileName = this.file.original_name
this.fileSize = formatSize(this.file.file_size)
},
methods: {
download,
},
}
})
const file_id = ref(0)
const ext = ref('')
const fileName = ref('')
const fileSize = ref('')
// 原 created 钩子
file_id.value = props.file.id
ext.value = props.file.file_suffix.toUpperCase()
fileName.value = props.file.original_name
fileSize.value = formatSize(props.file.file_size)
// download 直接从 @/utils/functions 导入,模板中照用
</script>
<style lang="less" scoped>
.file-message {

View File

@@ -17,48 +17,44 @@
<talk-forward-record ref="forwardRecordsRef" />
</div>
</template>
<script>
<script setup>
import { ref } from 'vue'
import TalkForwardRecord from '@/components/chat/TalkForwardRecord'
export default {
name: 'ForwardMessage',
components: {
TalkForwardRecord,
},
props: {
forward: {
type: Object,
required: true,
},
record_id: {
type: Number,
required: true,
default: 0,
},
},
data() {
return {
title: '',
records: [],
num: 0,
}
},
methods: {
catForwardRecords() {
this.$refs.forwardRecordsRef.open(this.record_id)
},
getForwardTitle(list) {
let arr = [...new Set(list.map(v => v.nickname))]
return arr.join('、') + '的会话记录'
},
},
created() {
let forward = this.forward
this.num = forward.num
this.records = forward.list
this.title = this.getForwardTitle(this.records)
defineOptions({ name: 'ForwardMessage' })
const props = defineProps({
forward: {
type: Object,
required: true,
},
record_id: {
type: Number,
required: true,
default: 0,
},
})
const forwardRecordsRef = ref(null)
const title = ref('')
const records = ref([])
const num = ref(0)
function catForwardRecords() {
forwardRecordsRef.value.open(props.record_id)
}
function getForwardTitle(list) {
let arr = [...new Set(list.map(v => v.nickname))]
return arr.join('、') + '的会话记录'
}
let forward = props.forward
num.value = forward.num
records.value = forward.list
title.value = getForwardTitle(records.value)
</script>
<style lang="less" scoped>
/* 会话记录消息 */

View File

@@ -18,33 +18,30 @@
</div>
</template>
<script>
export default {
name: 'FriendApplyMessage',
props: {
data: {
type: Object,
default() {
return {}
},
},
},
data() {
return {
avatar:
'http://im-img.gzydong.club/media/images/avatar/20210602/60b6f03598ed0104301.png',
nickname: '独特态度',
datetime: '05/09 12:13 分',
remarks: '编辑个签,展示我的独特态度 展示我的独特态度。',
apply_id: 0,
}
},
created() {},
methods: {
handle(type) {
alert(type)
<script setup>
import { ref } from 'vue'
defineOptions({ name: 'FriendApplyMessage' })
const props = defineProps({
data: {
type: Object,
default() {
return {}
},
},
})
const avatar = ref(
'http://im-img.gzydong.club/media/images/avatar/20210602/60b6f03598ed0104301.png'
)
const nickname = ref('独特态度')
const datetime = ref('05/09 12:13 分')
const remarks = ref('编辑个签,展示我的独特态度 展示我的独特态度。')
const apply_id = ref(0)
function handle(type) {
alert(type)
}
</script>
<style lang="less" scoped>

View File

@@ -16,21 +16,20 @@
</el-image>
</div>
</template>
<script>
<script setup>
import { imgZoom } from '@/utils/functions'
export default {
name: 'ImageMessage',
props: {
src: {
type: String,
default: '',
},
},
methods: {
getImgStyle(url) {
return imgZoom(url, 200)
},
defineOptions({ name: 'ImageMessage' })
defineProps({
src: {
type: String,
default: '',
},
})
function getImgStyle(url) {
return imgZoom(url, 200)
}
</script>
<style lang="less" scoped>

View File

@@ -20,20 +20,20 @@
</div>
</div>
</template>
<script>
export default {
name: 'InviteMessage',
props: {
invite: {
type: Object,
required: true,
},
},
methods: {
toUser(user_id) {
this.$emit('cat', user_id)
},
<script setup>
defineOptions({ name: 'InviteMessage' })
defineProps({
invite: {
type: Object,
required: true,
},
})
const emit = defineEmits(['cat'])
function toUser(user_id) {
emit('cat', user_id)
}
</script>
<style lang="less" scoped>

View File

@@ -10,76 +10,72 @@
</div>
</template>
<script>
<script setup>
import { ref } from 'vue'
import { parseTime } from '@/utils/functions'
export default {
name: 'LoginMessage',
props: {
detail: {
type: Object,
required: true,
},
},
data() {
return {
datetime: '',
ip: '',
address: '',
platform: '',
reason: '常用设备登录',
}
},
created() {
this.ip = this.detail.ip
this.datetime = parseTime(
this.detail.created_at,
'{y}年{m}月{d}日 {h}:{i}:{s}'
)
this.address = this.detail.address
this.reason = this.detail.reason
defineOptions({ name: 'LoginMessage' })
this.platform =
this.getExploreName(this.detail.agent) +
' / ' +
this.getExploreOs(this.detail.agent)
const props = defineProps({
detail: {
type: Object,
required: true,
},
methods: {
getExploreName(userAgent = '') {
if (userAgent.indexOf('Opera') > -1 || userAgent.indexOf('OPR') > -1) {
return 'Opera'
} else if (
userAgent.indexOf('compatible') > -1 &&
userAgent.indexOf('MSIE') > -1
) {
return 'IE'
} else if (userAgent.indexOf('Edge') > -1) {
return 'Edge'
} else if (userAgent.indexOf('Firefox') > -1) {
return 'Firefox'
} else if (
userAgent.indexOf('Safari') > -1 &&
userAgent.indexOf('Chrome') == -1
) {
return 'Safari'
} else if (
userAgent.indexOf('Chrome') > -1 &&
userAgent.indexOf('Safari') > -1
) {
return 'Chrome'
} else {
return 'Unkonwn'
}
},
})
getExploreOs(userAgent = '') {
if (userAgent.indexOf('Mac OS') > -1) {
return 'Mac OS'
} else {
return 'Windows'
}
},
},
const datetime = ref('')
const ip = ref('')
const address = ref('')
const platform = ref('')
const reason = ref('常用设备登录')
// 原 created 生命周期逻辑,setup 顶层直接执行
ip.value = props.detail.ip
datetime.value = parseTime(
props.detail.created_at,
'{y}年{m}月{d}日 {h}:{i}:{s}'
)
address.value = props.detail.address
reason.value = props.detail.reason
platform.value =
getExploreName(props.detail.agent) +
' / ' +
getExploreOs(props.detail.agent)
function getExploreName(userAgent = '') {
if (userAgent.indexOf('Opera') > -1 || userAgent.indexOf('OPR') > -1) {
return 'Opera'
} else if (
userAgent.indexOf('compatible') > -1 &&
userAgent.indexOf('MSIE') > -1
) {
return 'IE'
} else if (userAgent.indexOf('Edge') > -1) {
return 'Edge'
} else if (userAgent.indexOf('Firefox') > -1) {
return 'Firefox'
} else if (
userAgent.indexOf('Safari') > -1 &&
userAgent.indexOf('Chrome') == -1
) {
return 'Safari'
} else if (
userAgent.indexOf('Chrome') > -1 &&
userAgent.indexOf('Safari') > -1
) {
return 'Chrome'
} else {
return 'Unkonwn'
}
}
function getExploreOs(userAgent = '') {
if (userAgent.indexOf('Mac OS') > -1) {
return 'Mac OS'
} else {
return 'Windows'
}
}
</script>
<style lang="less" scoped>

View File

@@ -2,15 +2,10 @@
<div class="reply-message">这是回复的消息[预留]</div>
</template>
<script>
export default {
<script setup>
defineOptions({
name: 'ReplyMessage',
data() {
return {}
},
created() {},
methods: {},
}
})
</script>
<style lang="less" scoped>
.reply-message {

View File

@@ -1,7 +1,7 @@
<template>
<div class="revoke-message">
<div class="content">
<span v-if="$store.state.user.id == item.user_id">
<span v-if="userStore.id == item.user_id">
你撤回了一条消息 | {{ sendTime(item.created_at) }}
</span>
<span v-else-if="item.talk_type == 1">
@@ -13,20 +13,21 @@
</div>
</div>
</template>
<script>
<script setup>
import { useUserStore } from "@/stores/user";
import { formatTime as sendTime } from "@/utils/functions";
export default {
defineOptions({
name: "RevokeMessage",
props: {
item: {
type: Object,
},
});
const userStore = useUserStore();
defineProps({
item: {
type: Object,
},
methods: {
sendTime,
},
};
});
</script>
<style lang="less" scoped>
.revoke-message {

View File

@@ -3,18 +3,14 @@
<div class="content">{{ content }}</div>
</div>
</template>
<script>
<script setup>
import { formatTime as sendTime } from '@/utils/functions'
export default {
name: 'SystemTextMessage',
props: {
content: String,
},
methods: {
sendTime,
},
}
defineOptions({ name: 'SystemTextMessage' })
defineProps({
content: String,
})
</script>
<style lang="less" scoped>
.system-text-message {

View File

@@ -8,44 +8,42 @@
<pre v-html="html" />
</div>
</template>
<script>
<script setup>
import { ref } from "vue";
import { textReplaceLink } from "@/utils/functions";
import { textReplaceEmoji } from "@/utils/emojis";
export default {
defineOptions({
name: "TextMessage",
props: {
content: {
type: [String, Number],
default: "",
},
float: {
type: String,
default: "left",
},
fullWidth: {
type: Boolean,
default: true,
},
arrow: {
type: Boolean,
default: false,
},
},
data () {
return {
html: "",
};
},
created () {
const text = textReplaceLink(
this.content,
this.float == "right" ? "#ffffff" : "rgb(9 149 208)"
);
});
this.html = textReplaceEmoji(text);
const props = defineProps({
content: {
type: [String, Number],
default: "",
},
};
float: {
type: String,
default: "left",
},
fullWidth: {
type: Boolean,
default: true,
},
arrow: {
type: Boolean,
default: false,
},
});
const html = ref("");
const text = textReplaceLink(
props.content,
props.float == "right" ? "#ffffff" : "rgb(9 149 208)"
);
html.value = textReplaceEmoji(text);
</script>
<style lang="less" scoped>
@bg-left-color: #f5f5f5;

View File

@@ -2,18 +2,7 @@
<!-- 用户卡片消息 - 预留 -->
<div></div>
</template>
<script>
export default {
name: 'UserCardMessage',
components: {},
data() {
return {}
},
computed: {},
watch: {},
methods: {},
created() {},
}
<script setup>
defineOptions({ name: 'UserCardMessage' })
</script>
<style lang="less" scoped></style>

View File

@@ -4,15 +4,7 @@
</div>
</template>
<script>
export default {
name: 'VideoMessage',
components: {},
data() {
return {}
},
methods: {},
created() {},
}
<script setup>
defineOptions({ name: 'VideoMessage' })
</script>
<style lang="less" scoped></style>

View File

@@ -20,27 +20,27 @@
</div>
</div>
</template>
<script>
export default {
name: 'VisitCardMessage',
data() {
return {
userId: 0,
friendId: 0,
avatar:
'http://im-serve0.gzydong.club/static/image/sys-head/2019012107542668696.jpg',
sign:
'这个社会,是赢家通吃,输者一无所有,社会,永远都是只以成败论英雄。',
nickname:
'氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳',
}
},
created() {},
methods: {
openVisitCard(user_id) {
this.$emit('openVisitCard', user_id)
},
},
<script setup>
import { ref } from "vue";
defineOptions({ name: 'VisitCardMessage' });
const emit = defineEmits(['openVisitCard']);
const userId = ref(0);
const friendId = ref(0);
const avatar = ref(
'http://im-serve0.gzydong.club/static/image/sys-head/2019012107542668696.jpg'
);
const sign = ref(
'这个社会,是赢家通吃,输者一无所有,社会,永远都是只以成败论英雄。'
);
const nickname = ref(
'氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳'
);
function openVisitCard(user_id) {
emit('openVisitCard', user_id)
}
</script>
<style lang="less" scoped>

View File

@@ -2,15 +2,9 @@
<div class="voice-message"></div>
</template>
<script>
export default {
<script setup>
defineOptions({
name: 'VoiceMessage',
components: {},
data() {
return {}
},
methods: {},
created() {},
}
})
</script>
<style lang="less" scoped></style>

View File

@@ -59,127 +59,128 @@
</div>
</template>
<script>
<script setup>
import { ref, computed } from "vue";
import { ServeConfirmVoteHandle } from "@/api/chat";
import { useUserStore } from "@/stores/user";
export default {
name: "VoteMessage",
props: {
vote: {
type: Object,
required: true,
},
record_id: {
type: Number,
required: true,
},
},
data() {
return {
answer_mode: 0,
title: "啊谁叫你打开你卡沙发那,那就是看、卡收纳是你",
radio_value: "",
options: [],
is_vote: false,
answer_num: 0,
answered_num: 0,
refresh: false,
};
},
computed: {
isUserVote() {
return this.options.some((iten) => {
return iten.is_checked;
});
},
},
created() {
let user_id = this.$store.state.user.id;
let { detail, statistics, vote_users } = this.vote;
defineOptions({ name: "VoteMessage" });
this.answer_mode = detail.answer_mode;
this.answer_num = detail.answer_num;
this.answered_num = detail.answered_num;
const props = defineProps({
vote: {
type: Object,
required: true,
},
record_id: {
type: Number,
required: true,
},
});
detail.answer_option.forEach((item) => {
this.options.push({
value: item.key,
text: item.value,
is_checked: false,
num: 0,
progress: "00.0",
});
const userStore = useUserStore();
const answer_mode = ref(0);
const title = ref("啊谁叫你打开你卡沙发那,那就是看、卡收纳是你");
const radio_value = ref("");
const options = ref([]);
const is_vote = ref(false);
const answer_num = ref(0);
const answered_num = ref(0);
const refresh = ref(false);
const isUserVote = computed(() => {
return options.value.some((iten) => {
return iten.is_checked;
});
});
function loadRefresh() {
refresh.value = true;
setTimeout(() => {
refresh.value = false;
}, 500);
}
function updateStatistics(data) {
let count = data.count;
options.value.forEach((option) => {
option.num = data.options[option.value];
if (count > 0) {
option.progress = (data.options[option.value] / count) * 100;
}
});
}
function toSelect(option, index) {
if (answer_mode.value == 0) {
options.value.forEach((option) => {
option.is_checked = false;
});
}
this.is_vote = vote_users.some((value) => {
return value == user_id;
options.value[index].is_checked = !option.is_checked;
}
function toSelect2(option) {
if (answer_mode.value == 0) {
options.value.forEach((item) => {
if (option.value == item.value) {
item.is_checked = option.is_checked;
} else {
item.is_checked = false;
}
});
}
}
this.updateStatistics(statistics);
},
methods: {
loadRefresh() {
this.refresh = true;
function toVote() {
if (isUserVote.value == false) {
return false;
}
setTimeout(() => {
this.refresh = false;
}, 500);
},
updateStatistics(data) {
let count = data.count;
this.options.forEach((option) => {
option.num = data.options[option.value];
let items = [];
options.value.forEach((item) => {
if (item.is_checked) {
items.push(item.value);
}
});
if (count > 0) {
option.progress = (data.options[option.value] / count) * 100;
}
});
},
ServeConfirmVoteHandle({
record_id: props.record_id,
options: items.join(","),
}).then((res) => {
if (res.code == 200) {
is_vote.value = true;
updateStatistics(res.data);
}
});
}
toSelect(option, index) {
if (this.answer_mode == 0) {
this.options.forEach((option) => {
option.is_checked = false;
});
}
// 原 created() 生命周期逻辑
let user_id = userStore.id;
let { detail, statistics, vote_users } = props.vote;
this.options[index].is_checked = !option.is_checked;
},
toSelect2(option) {
if (this.answer_mode == 0) {
this.options.forEach((item) => {
if (option.value == item.value) {
item.is_checked = option.is_checked;
} else {
item.is_checked = false;
}
});
}
},
toVote() {
if (this.isUserVote == false) {
return false;
}
answer_mode.value = detail.answer_mode;
answer_num.value = detail.answer_num;
answered_num.value = detail.answered_num;
let items = [];
this.options.forEach((item) => {
if (item.is_checked) {
items.push(item.value);
}
});
detail.answer_option.forEach((item) => {
options.value.push({
value: item.key,
text: item.value,
is_checked: false,
num: 0,
progress: "00.0",
});
});
ServeConfirmVoteHandle({
record_id: this.record_id,
options: items.join(","),
}).then((res) => {
if (res.code == 200) {
this.is_vote = true;
this.updateStatistics(res.data);
}
});
},
},
};
is_vote.value = vote_users.some((value) => {
return value == user_id;
});
updateStatistics(statistics);
</script>
<style lang="less" scoped>
.vote-message {