优化二维码池筛选和日期中文化
This commit is contained in:
@@ -3,6 +3,8 @@ package chat
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -61,6 +63,12 @@ type UpdateQrCodeRequest struct {
|
|||||||
// QrCodeListRequest 列表查询请求
|
// QrCodeListRequest 列表查询请求
|
||||||
type QrCodeListRequest struct {
|
type QrCodeListRequest struct {
|
||||||
Status string `form:"status"`
|
Status string `form:"status"`
|
||||||
|
Keyword string `form:"keyword"`
|
||||||
|
WecomRenamed string `form:"wecom_renamed"`
|
||||||
|
Bound string `form:"bound"`
|
||||||
|
ExpireStatus string `form:"expire_status"`
|
||||||
|
CreatedStart string `form:"created_start"`
|
||||||
|
CreatedEnd string `form:"created_end"`
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Limit int `form:"limit"`
|
Limit int `form:"limit"`
|
||||||
}
|
}
|
||||||
@@ -167,32 +175,28 @@ func (r *Repository) ListQrCodes(ctx context.Context, req QrCodeListRequest) ([]
|
|||||||
req.Limit = 20
|
req.Limit = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).Model(&model.ChatQrCode{})
|
now := time.Now()
|
||||||
|
baseQuery := func() *gorm.DB {
|
||||||
// 状态过滤
|
return applyQrCodeListFilters(
|
||||||
if req.Status != "" {
|
r.db.WithContext(ctx).
|
||||||
query = query.Where("status = ?", req.Status)
|
Table("chat_qrcode_pool AS q").
|
||||||
|
Joins("LEFT JOIN chat_conversations AS c ON c.id = q.conversation_id"),
|
||||||
|
req,
|
||||||
|
now,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计总数
|
// 统计总数
|
||||||
var total int64
|
var total int64
|
||||||
if err := query.Count(&total).Error; err != nil {
|
if err := baseQuery().Count(&total).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询列表
|
// 查询列表
|
||||||
var qrcodes []QrCodeListItem
|
var qrcodes []QrCodeListItem
|
||||||
offset := (req.Page - 1) * req.Limit
|
offset := (req.Page - 1) * req.Limit
|
||||||
if err := r.db.WithContext(ctx).
|
if err := baseQuery().
|
||||||
Table("chat_qrcode_pool AS q").
|
|
||||||
Select("q.*, COALESCE(c.title, '') AS bound_conversation_title").
|
Select("q.*, COALESCE(c.title, '') AS bound_conversation_title").
|
||||||
Joins("LEFT JOIN chat_conversations AS c ON c.id = q.conversation_id").
|
|
||||||
Scopes(func(db *gorm.DB) *gorm.DB {
|
|
||||||
if req.Status != "" {
|
|
||||||
return db.Where("q.status = ?", req.Status)
|
|
||||||
}
|
|
||||||
return db
|
|
||||||
}).
|
|
||||||
Order("q.id DESC").
|
Order("q.id DESC").
|
||||||
Offset(offset).
|
Offset(offset).
|
||||||
Limit(req.Limit).
|
Limit(req.Limit).
|
||||||
@@ -203,6 +207,88 @@ func (r *Repository) ListQrCodes(ctx context.Context, req QrCodeListRequest) ([]
|
|||||||
return qrcodes, total, nil
|
return qrcodes, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyQrCodeListFilters(query *gorm.DB, req QrCodeListRequest, now time.Time) *gorm.DB {
|
||||||
|
if req.Status != "" {
|
||||||
|
query = query.Where("q.status = ?", req.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyword := strings.TrimSpace(req.Keyword); keyword != "" {
|
||||||
|
like := "%" + keyword + "%"
|
||||||
|
conditions := "(q.group_name LIKE ? OR q.note LIKE ? OR COALESCE(c.title, '') LIKE ?"
|
||||||
|
args := []interface{}{like, like, like}
|
||||||
|
if id, err := strconv.ParseUint(keyword, 10, 64); err == nil {
|
||||||
|
conditions += " OR q.id = ? OR q.conversation_id = ?"
|
||||||
|
args = append(args, id, id)
|
||||||
|
}
|
||||||
|
conditions += ")"
|
||||||
|
query = query.Where(conditions, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := parseQrCodeBoolFilter(req.WecomRenamed); ok {
|
||||||
|
query = query.Where("q.wecom_renamed = ?", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := parseQrCodeBoolFilter(req.Bound); ok {
|
||||||
|
if value {
|
||||||
|
query = query.Where("q.conversation_id IS NOT NULL")
|
||||||
|
} else {
|
||||||
|
query = query.Where("q.conversation_id IS NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.ExpireStatus {
|
||||||
|
case "valid":
|
||||||
|
query = query.Where("(q.expires_at IS NULL OR q.expires_at > ?)", now)
|
||||||
|
case "expired":
|
||||||
|
query = query.Where("q.expires_at IS NOT NULL AND q.expires_at <= ?", now)
|
||||||
|
case "permanent":
|
||||||
|
query = query.Where("q.expires_at IS NULL")
|
||||||
|
case "expiring_soon":
|
||||||
|
query = query.Where("q.expires_at > ? AND q.expires_at <= ?", now, now.Add(72*time.Hour))
|
||||||
|
}
|
||||||
|
|
||||||
|
if start, ok := parseQrCodeFilterTime(req.CreatedStart); ok {
|
||||||
|
query = query.Where("q.created_at >= ?", start)
|
||||||
|
}
|
||||||
|
if end, ok := parseQrCodeFilterTime(req.CreatedEnd); ok {
|
||||||
|
query = query.Where("q.created_at <= ?", end)
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseQrCodeBoolFilter(value string) (bool, bool) {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
|
case "true", "1", "yes":
|
||||||
|
return true, true
|
||||||
|
case "false", "0", "no":
|
||||||
|
return false, true
|
||||||
|
default:
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseQrCodeFilterTime(value string) (time.Time, bool) {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" {
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
layouts := []string{
|
||||||
|
time.RFC3339,
|
||||||
|
"2006-01-02T15:04:05",
|
||||||
|
"2006-01-02T15:04",
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
"2006-01-02 15:04",
|
||||||
|
"2006-01-02",
|
||||||
|
}
|
||||||
|
for _, layout := range layouts {
|
||||||
|
if parsed, err := time.Parse(layout, value); err == nil {
|
||||||
|
return parsed, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
|
||||||
// GetQrCodeStats 获取二维码统计信息
|
// GetQrCodeStats 获取二维码统计信息
|
||||||
func (r *Repository) GetQrCodeStats(ctx context.Context) (*QrCodeStats, error) {
|
func (r *Repository) GetQrCodeStats(ctx context.Context) (*QrCodeStats, error) {
|
||||||
stats := &QrCodeStats{}
|
stats := &QrCodeStats{}
|
||||||
|
|||||||
@@ -104,6 +104,102 @@ func TestListQrCodesIncludesBoundConversationTitle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListQrCodesFilters(t *testing.T) {
|
||||||
|
db := setupQrCodeTestDB(t)
|
||||||
|
repo := NewRepository(db, nil)
|
||||||
|
now := time.Now()
|
||||||
|
expiredAt := now.Add(-time.Hour)
|
||||||
|
validAt := now.Add(7 * 24 * time.Hour)
|
||||||
|
expiringAt := now.Add(2 * time.Hour)
|
||||||
|
conversation := model.ChatConversation{
|
||||||
|
Title: "账号群 绑定测试",
|
||||||
|
Type: ConversationTypeListingGroup,
|
||||||
|
Status: "active",
|
||||||
|
}
|
||||||
|
if err := db.Create(&conversation).Error; err != nil {
|
||||||
|
t.Fatalf("创建群聊失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
qrcodes := []model.ChatQrCode{
|
||||||
|
{
|
||||||
|
ImageURL: "/api/files/object?key=qrcode/unused-a.png",
|
||||||
|
GroupName: "待用群A",
|
||||||
|
Status: QrCodeStatusUnused,
|
||||||
|
CreatedBy: 1,
|
||||||
|
Note: "六月首批",
|
||||||
|
ExpiresAt: &validAt,
|
||||||
|
CreatedAt: now.Add(-48 * time.Hour),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageURL: "/api/files/object?key=qrcode/used-b.png",
|
||||||
|
GroupName: "已发群B",
|
||||||
|
Status: QrCodeStatusUsed,
|
||||||
|
ConversationID: &conversation.ID,
|
||||||
|
CreatedBy: 1,
|
||||||
|
Note: "特殊备注",
|
||||||
|
WecomRenamed: true,
|
||||||
|
ExpiresAt: &expiredAt,
|
||||||
|
CreatedAt: now.Add(-2 * time.Hour),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageURL: "/api/files/object?key=qrcode/permanent-c.png",
|
||||||
|
GroupName: "永久群C",
|
||||||
|
Status: QrCodeStatusDisabled,
|
||||||
|
CreatedBy: 1,
|
||||||
|
Note: "长期备用",
|
||||||
|
CreatedAt: now.Add(-24 * time.Hour),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ImageURL: "/api/files/object?key=qrcode/expiring-d.png",
|
||||||
|
GroupName: "临期群D",
|
||||||
|
Status: QrCodeStatusUnused,
|
||||||
|
CreatedBy: 1,
|
||||||
|
Note: "三天内到期",
|
||||||
|
ExpiresAt: &expiringAt,
|
||||||
|
CreatedAt: now.Add(-30 * time.Minute),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := db.Create(&qrcodes).Error; err != nil {
|
||||||
|
t.Fatalf("创建二维码失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertListIDs := func(name string, req QrCodeListRequest, want ...uint64) {
|
||||||
|
t.Helper()
|
||||||
|
req.Page = 1
|
||||||
|
req.Limit = 20
|
||||||
|
items, total, err := repo.ListQrCodes(t.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s 查询二维码失败: %v", name, err)
|
||||||
|
}
|
||||||
|
if total != int64(len(want)) || len(items) != len(want) {
|
||||||
|
t.Fatalf("%s total = %d, len = %d, want %d", name, total, len(items), len(want))
|
||||||
|
}
|
||||||
|
for i, id := range want {
|
||||||
|
if items[i].ID != id {
|
||||||
|
t.Fatalf("%s 第 %d 条 ID = %d, want %d", name, i, items[i].ID, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertListIDs("状态", QrCodeListRequest{Status: QrCodeStatusUnused}, qrcodes[3].ID, qrcodes[0].ID)
|
||||||
|
assertListIDs("关键词群名", QrCodeListRequest{Keyword: "待用群"}, qrcodes[0].ID)
|
||||||
|
assertListIDs("关键词备注", QrCodeListRequest{Keyword: "特殊备注"}, qrcodes[1].ID)
|
||||||
|
assertListIDs("关键词绑定群", QrCodeListRequest{Keyword: "绑定测试"}, qrcodes[1].ID)
|
||||||
|
assertListIDs("企微改名", QrCodeListRequest{WecomRenamed: "true"}, qrcodes[1].ID)
|
||||||
|
assertListIDs("未绑定", QrCodeListRequest{Bound: "false"}, qrcodes[3].ID, qrcodes[2].ID, qrcodes[0].ID)
|
||||||
|
assertListIDs("已过期", QrCodeListRequest{ExpireStatus: "expired"}, qrcodes[1].ID)
|
||||||
|
assertListIDs("永久有效", QrCodeListRequest{ExpireStatus: "permanent"}, qrcodes[2].ID)
|
||||||
|
assertListIDs("即将过期", QrCodeListRequest{ExpireStatus: "expiring_soon"}, qrcodes[3].ID)
|
||||||
|
assertListIDs(
|
||||||
|
"上传时间",
|
||||||
|
QrCodeListRequest{
|
||||||
|
CreatedStart: now.Add(-3 * time.Hour).Format(time.RFC3339),
|
||||||
|
CreatedEnd: now.Add(-90 * time.Minute).Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
qrcodes[1].ID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseQrCodeGroupNameCandidates(t *testing.T) {
|
func TestParseQrCodeGroupNameCandidates(t *testing.T) {
|
||||||
rawText := strings.Join([]string{
|
rawText := strings.Join([]string{
|
||||||
"使用微信或企业微信扫码加入",
|
"使用微信或企业微信扫码加入",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterView } from 'vue-router'
|
import { ElConfigProvider } from 'element-plus'
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { RouterView, useRoute } from 'vue-router'
|
||||||
|
|
||||||
import InAppBrowserPrompt from './components/InAppBrowserPrompt.vue'
|
import InAppBrowserPrompt from './components/InAppBrowserPrompt.vue'
|
||||||
import AdminLayout from './layouts/AdminLayout.vue'
|
import AdminLayout from './layouts/AdminLayout.vue'
|
||||||
@@ -18,6 +19,7 @@ const layout = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ElConfigProvider :locale="zhCn">
|
||||||
<AdminLayout v-if="layout === 'admin'">
|
<AdminLayout v-if="layout === 'admin'">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
@@ -26,4 +28,5 @@ const layout = computed(() => {
|
|||||||
<RouterView />
|
<RouterView />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
<InAppBrowserPrompt />
|
<InAppBrowserPrompt />
|
||||||
|
</ElConfigProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { apiClient } from '@/shared/api/client'
|
|||||||
import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
|
import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
|
||||||
|
|
||||||
export type QrCodeStatus = 'unused' | 'used' | 'disabled'
|
export type QrCodeStatus = 'unused' | 'used' | 'disabled'
|
||||||
|
export type QrCodeBooleanFilter = 'true' | 'false'
|
||||||
|
export type QrCodeExpireStatus = 'valid' | 'expired' | 'permanent' | 'expiring_soon'
|
||||||
|
|
||||||
export interface ChatQrCode {
|
export interface ChatQrCode {
|
||||||
id: number
|
id: number
|
||||||
@@ -34,6 +36,12 @@ export interface QrCodeOcrResult {
|
|||||||
|
|
||||||
export interface QrCodeListQuery {
|
export interface QrCodeListQuery {
|
||||||
status?: QrCodeStatus
|
status?: QrCodeStatus
|
||||||
|
keyword?: string
|
||||||
|
wecom_renamed?: QrCodeBooleanFilter
|
||||||
|
bound?: QrCodeBooleanFilter
|
||||||
|
expire_status?: QrCodeExpireStatus
|
||||||
|
created_start?: string
|
||||||
|
created_end?: string
|
||||||
page?: number
|
page?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
@@ -63,7 +71,17 @@ export async function fetchQrCodes(query: QrCodeListQuery = {}) {
|
|||||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<ChatQrCode>>>(
|
const { data } = await apiClient.get<ApiResponse<PaginatedResult<ChatQrCode>>>(
|
||||||
'/admin/chats/qrcodes',
|
'/admin/chats/qrcodes',
|
||||||
{
|
{
|
||||||
params: { status: query.status || undefined, page: query.page || 1, limit: query.limit || 20 },
|
params: {
|
||||||
|
status: query.status || undefined,
|
||||||
|
keyword: query.keyword?.trim() || undefined,
|
||||||
|
wecom_renamed: query.wecom_renamed || undefined,
|
||||||
|
bound: query.bound || undefined,
|
||||||
|
expire_status: query.expire_status || undefined,
|
||||||
|
created_start: query.created_start || undefined,
|
||||||
|
created_end: query.created_end || undefined,
|
||||||
|
page: query.page || 1,
|
||||||
|
limit: query.limit || 20,
|
||||||
|
},
|
||||||
silent: true,
|
silent: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
updateQrCode,
|
updateQrCode,
|
||||||
type ChatQrCode,
|
type ChatQrCode,
|
||||||
type CreateQrCodePayload,
|
type CreateQrCodePayload,
|
||||||
|
type QrCodeBooleanFilter,
|
||||||
|
type QrCodeExpireStatus,
|
||||||
type QrCodeStatus,
|
type QrCodeStatus,
|
||||||
type QrCodeStats,
|
type QrCodeStats,
|
||||||
} from '@/features/admin/api/adminQrCodes'
|
} from '@/features/admin/api/adminQrCodes'
|
||||||
@@ -40,7 +42,24 @@ const canManageQrCodes = computed(() => adminSession.hasPermission('qrcode:manag
|
|||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const currentPageSize = ref(20)
|
const currentPageSize = ref(20)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const statusFilter = ref<QrCodeStatus | ''>('')
|
|
||||||
|
type QrCodeFilters = {
|
||||||
|
keyword: string
|
||||||
|
status: QrCodeStatus | ''
|
||||||
|
wecom_renamed: QrCodeBooleanFilter | ''
|
||||||
|
bound: QrCodeBooleanFilter | ''
|
||||||
|
expire_status: QrCodeExpireStatus | ''
|
||||||
|
created_range: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = reactive<QrCodeFilters>({
|
||||||
|
keyword: '',
|
||||||
|
status: '',
|
||||||
|
wecom_renamed: '',
|
||||||
|
bound: '',
|
||||||
|
expire_status: '',
|
||||||
|
created_range: [],
|
||||||
|
})
|
||||||
|
|
||||||
// 上传弹窗
|
// 上传弹窗
|
||||||
const uploadVisible = ref(false)
|
const uploadVisible = ref(false)
|
||||||
@@ -81,6 +100,23 @@ const statusOptions: { label: string; value: QrCodeStatus | ''; type: string }[]
|
|||||||
{ label: '已停用', value: 'disabled', type: 'danger' },
|
{ label: '已停用', value: 'disabled', type: 'danger' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const renameFilterOptions: { label: string; value: QrCodeBooleanFilter }[] = [
|
||||||
|
{ label: '已改名', value: 'true' },
|
||||||
|
{ label: '未改名', value: 'false' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const boundFilterOptions: { label: string; value: QrCodeBooleanFilter }[] = [
|
||||||
|
{ label: '已绑定', value: 'true' },
|
||||||
|
{ label: '未绑定', value: 'false' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const expireStatusOptions: { label: string; value: QrCodeExpireStatus }[] = [
|
||||||
|
{ label: '未过期', value: 'valid' },
|
||||||
|
{ label: '即将过期', value: 'expiring_soon' },
|
||||||
|
{ label: '已过期', value: 'expired' },
|
||||||
|
{ label: '永久有效', value: 'permanent' },
|
||||||
|
]
|
||||||
|
|
||||||
const statusMap = computed(() => {
|
const statusMap = computed(() => {
|
||||||
const m: Record<string, { label: string; type: string }> = {}
|
const m: Record<string, { label: string; type: string }> = {}
|
||||||
statusOptions.forEach(o => {
|
statusOptions.forEach(o => {
|
||||||
@@ -104,11 +140,25 @@ const selectedRows = ref<ChatQrCode[]>([])
|
|||||||
const selectedCount = computed(() => selectedRows.value.length)
|
const selectedCount = computed(() => selectedRows.value.length)
|
||||||
const selectedIssuedCount = computed(() => selectedRows.value.filter(qrcodeWasIssued).length)
|
const selectedIssuedCount = computed(() => selectedRows.value.filter(qrcodeWasIssued).length)
|
||||||
|
|
||||||
|
function filterDateToISO(value?: string) {
|
||||||
|
if (!value) return undefined
|
||||||
|
const date = new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) return undefined
|
||||||
|
return date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
async function loadList() {
|
async function loadList() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
|
const [createdStart, createdEnd] = filters.created_range || []
|
||||||
const res = await fetchQrCodes({
|
const res = await fetchQrCodes({
|
||||||
status: statusFilter.value || undefined,
|
status: filters.status || undefined,
|
||||||
|
keyword: filters.keyword.trim() || undefined,
|
||||||
|
wecom_renamed: filters.wecom_renamed || undefined,
|
||||||
|
bound: filters.bound || undefined,
|
||||||
|
expire_status: filters.expire_status || undefined,
|
||||||
|
created_start: filterDateToISO(createdStart),
|
||||||
|
created_end: filterDateToISO(createdEnd),
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
limit: currentPageSize.value,
|
limit: currentPageSize.value,
|
||||||
})
|
})
|
||||||
@@ -133,6 +183,16 @@ function handleFilter() {
|
|||||||
loadList()
|
loadList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
filters.keyword = ''
|
||||||
|
filters.status = ''
|
||||||
|
filters.wecom_renamed = ''
|
||||||
|
filters.bound = ''
|
||||||
|
filters.expire_status = ''
|
||||||
|
filters.created_range = []
|
||||||
|
handleFilter()
|
||||||
|
}
|
||||||
|
|
||||||
function handlePageChange() {
|
function handlePageChange() {
|
||||||
loadList()
|
loadList()
|
||||||
}
|
}
|
||||||
@@ -452,11 +512,20 @@ onMounted(reloadAll)
|
|||||||
<el-card shadow="never" class="toolbar-card">
|
<el-card shadow="never" class="toolbar-card">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="filter-area">
|
<div class="filter-area">
|
||||||
|
<el-input
|
||||||
|
v-model="filters.keyword"
|
||||||
|
clearable
|
||||||
|
class="keyword-filter"
|
||||||
|
:prefix-icon="Search"
|
||||||
|
placeholder="群名 / 备注 / 绑定群 / ID"
|
||||||
|
@clear="handleFilter"
|
||||||
|
@keyup.enter="handleFilter"
|
||||||
|
/>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="statusFilter"
|
v-model="filters.status"
|
||||||
placeholder="状态筛选"
|
placeholder="状态筛选"
|
||||||
clearable
|
clearable
|
||||||
style="width: 140px"
|
class="compact-filter"
|
||||||
@change="handleFilter"
|
@change="handleFilter"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
@@ -466,7 +535,61 @@ onMounted(reloadAll)
|
|||||||
:value="opt.value"
|
:value="opt.value"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button :icon="Search" @click="handleFilter">筛选</el-button>
|
<el-select
|
||||||
|
v-model="filters.wecom_renamed"
|
||||||
|
placeholder="企微改名"
|
||||||
|
clearable
|
||||||
|
class="compact-filter"
|
||||||
|
@change="handleFilter"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="opt in renameFilterOptions"
|
||||||
|
:key="`rename-${opt.value}`"
|
||||||
|
:label="opt.label"
|
||||||
|
:value="opt.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-select
|
||||||
|
v-model="filters.bound"
|
||||||
|
placeholder="绑定群"
|
||||||
|
clearable
|
||||||
|
class="compact-filter"
|
||||||
|
@change="handleFilter"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="opt in boundFilterOptions"
|
||||||
|
:key="`bound-${opt.value}`"
|
||||||
|
:label="opt.label"
|
||||||
|
:value="opt.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-select
|
||||||
|
v-model="filters.expire_status"
|
||||||
|
placeholder="过期状态"
|
||||||
|
clearable
|
||||||
|
class="compact-filter"
|
||||||
|
@change="handleFilter"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="opt in expireStatusOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:label="opt.label"
|
||||||
|
:value="opt.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="filters.created_range"
|
||||||
|
type="datetimerange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="上传开始"
|
||||||
|
end-placeholder="上传结束"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||||
|
class="date-range-filter"
|
||||||
|
@change="handleFilter"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" :icon="Search" @click="handleFilter">筛选</el-button>
|
||||||
|
<el-button @click="resetFilters">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-area">
|
<div class="action-area">
|
||||||
<span v-if="selectedCount" class="selection-summary">已选 {{ selectedCount }} 条</span>
|
<span v-if="selectedCount" class="selection-summary">已选 {{ selectedCount }} 条</span>
|
||||||
@@ -833,7 +956,7 @@ onMounted(reloadAll)
|
|||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
@@ -845,6 +968,29 @@ onMounted(reloadAll)
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-area {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-area {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyword-filter {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-filter {
|
||||||
|
width: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range-filter {
|
||||||
|
width: 330px;
|
||||||
|
}
|
||||||
|
|
||||||
.qrcode-table {
|
.qrcode-table {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -992,6 +1138,30 @@ onMounted(reloadAll)
|
|||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-area {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.keyword-filter,
|
||||||
|
.compact-filter,
|
||||||
|
.date-range-filter {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-area,
|
||||||
|
.action-area {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.upload-preview {
|
.upload-preview {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user