diff --git a/code/base-project/base-go/internal/app/handle/test.go b/code/base-project/base-go/internal/app/handle/test.go index 91a3bcc1..4770b4bf 100644 --- a/code/base-project/base-go/internal/app/handle/test.go +++ b/code/base-project/base-go/internal/app/handle/test.go @@ -5,4 +5,5 @@ import ( ) func TestDB(c *router.Context) { + } diff --git a/code/base-project/base-vue/package.json b/code/base-project/base-vue/package.json index bd8966e7..50935d6d 100644 --- a/code/base-project/base-vue/package.json +++ b/code/base-project/base-vue/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@ao/ao-design": "workspace:*", + "@floating-ui/vue": "^1.1.9", "@iconify/vue": "5.0.0", "naive-ui": "^2.42.0", "vue": "^3.5.18" diff --git a/code/base-project/base-vue/packages/ao-design/src/index.scss b/code/base-project/base-vue/packages/ao-design/src/index.scss index e0ee3e3b..09272773 100644 --- a/code/base-project/base-vue/packages/ao-design/src/index.scss +++ b/code/base-project/base-vue/packages/ao-design/src/index.scss @@ -4,5 +4,5 @@ @use './input/index.scss' as *; @use './textarea/index.scss' as *; @use './group/index.scss' as *; -@use "./popup/tooltip.scss" as *; +@use "./popup/index.scss" as *; diff --git a/code/base-project/base-vue/packages/ao-design/src/popup/index.scss b/code/base-project/base-vue/packages/ao-design/src/popup/index.scss new file mode 100644 index 00000000..2468d012 --- /dev/null +++ b/code/base-project/base-vue/packages/ao-design/src/popup/index.scss @@ -0,0 +1,10 @@ +@use '../_styles/_variables.scss' as *; + +$popup-prefix: #{$prefix}-popup; + +.#{$popup-prefix} { + position: absolute; + top: 0px; + left: 0px; + width: 100%; +} diff --git a/code/base-project/base-vue/packages/ao-design/src/popup/index.tsx b/code/base-project/base-vue/packages/ao-design/src/popup/index.tsx index 2a63ff4c..5a694975 100644 --- a/code/base-project/base-vue/packages/ao-design/src/popup/index.tsx +++ b/code/base-project/base-vue/packages/ao-design/src/popup/index.tsx @@ -1,158 +1,15 @@ -// Tooltip.tsx -import type { PropType } from 'vue' -import { cloneVNode, defineComponent, nextTick, onBeforeUnmount, onMounted, ref } from 'vue' - -interface Position { - top: number - left: number -} +import { defineComponent } from 'vue' +import { getPrefixCls } from '../_utils/getPrefixCls' export default defineComponent({ - name: 'Tooltip', - props: { - content: { type: String, required: true }, - placement: { type: String as PropType<'top' | 'bottom' | 'left' | 'right'>, default: 'top' }, - trigger: { type: String as PropType<'hover' | 'click'>, default: 'hover' }, - offset: { type: Number, default: 6 }, - }, + name: 'Popup', setup(props, { slots }) { - const visible = ref(false) - const targetEl = ref(null) - const tooltipEl = ref(null) + const prefixCls = getPrefixCls('popup') - const createTooltip = () => { - const el = document.createElement('div') - el.className = `tooltip tooltip-${props.placement}` - - const contentEl = document.createElement('div') - contentEl.className = 'tooltip-content' - contentEl.textContent = props.content - - const arrowEl = document.createElement('div') - arrowEl.className = 'tooltip-arrow' - - el.appendChild(contentEl) - el.appendChild(arrowEl) - - // 初始状态 - el.style.position = 'absolute' - el.style.top = '0' - el.style.left = '0' - el.style.opacity = '0' - el.style.visibility = 'hidden' - el.style.pointerEvents = 'none' - - document.body.appendChild(el) - tooltipEl.value = el - } - - const getTargetDom = (): HTMLElement | null => { - if (!targetEl.value) - return null - let dom = targetEl.value - if ('$el' in dom) - dom = (dom as any).$el - return dom instanceof HTMLElement ? dom : null - } - - const updatePosition = () => { - const tooltip = tooltipEl.value - const target = getTargetDom() - if (!tooltip || !target) - return - - const rect = target.getBoundingClientRect() - const tooltipRect = tooltip.getBoundingClientRect() - const scrollX = window.scrollX || window.pageXOffset - const scrollY = window.scrollY || window.pageYOffset - - let top = 0 - let left = 0 - const offset = props.offset - - switch (props.placement) { - case 'top': - top = rect.top + scrollY - tooltipRect.height - offset - left = rect.left + scrollX + rect.width / 2 - tooltipRect.width / 2 - break - case 'bottom': - top = rect.bottom + scrollY + offset - left = rect.left + scrollX + rect.width / 2 - tooltipRect.width / 2 - break - case 'left': - top = rect.top + scrollY + rect.height / 2 - tooltipRect.height / 2 - left = rect.left + scrollX - tooltipRect.width - offset - break - case 'right': - top = rect.top + scrollY + rect.height / 2 - tooltipRect.height / 2 - left = rect.right + scrollX + offset - break - } - - tooltip.style.top = `${top}px` - tooltip.style.left = `${left}px` - } - - const show = () => { - const tooltip = tooltipEl.value - if (!tooltip || !getTargetDom()) - return - - tooltip.style.visibility = 'visible' - tooltip.style.pointerEvents = 'auto' - - nextTick(() => { - updatePosition() - tooltip.style.opacity = '1' - tooltip.style.transform = 'scale(1)' - }) - visible.value = true - } - - const hide = () => { - const tooltip = tooltipEl.value - if (!tooltip) - return - tooltip.style.opacity = '0' - tooltip.style.transform = 'scale(0.95)' - tooltip.style.pointerEvents = 'none' - tooltip.style.visibility = 'hidden' - visible.value = false - } - - const toggle = () => (visible.value ? hide() : show()) - - onMounted(() => { - createTooltip() - - const onUpdate = () => { - if (visible.value) - updatePosition() - } - - window.addEventListener('scroll', onUpdate) - window.addEventListener('resize', onUpdate) - - onBeforeUnmount(() => { - window.removeEventListener('scroll', onUpdate) - window.removeEventListener('resize', onUpdate) - if (tooltipEl.value && tooltipEl.value.parentNode) { - tooltipEl.value.parentNode.removeChild(tooltipEl.value) - } - }) - }) - - return () => { - const child = slots.default?.()[0] - if (!child) - return null - - return cloneVNode(child, { - ref: targetEl, - onMouseenter: props.trigger === 'hover' ? show : undefined, - onMouseleave: props.trigger === 'hover' ? hide : undefined, - onClick: props.trigger === 'click' ? toggle : undefined, - }) - } + return () => ( +
+ {/* {slots?.default()} */} +
+ ) }, }) diff --git a/code/base-project/base-vue/packages/ao-design/src/popup/tooltip.scss b/code/base-project/base-vue/packages/ao-design/src/popup/tooltip.scss deleted file mode 100644 index 7bc9393c..00000000 --- a/code/base-project/base-vue/packages/ao-design/src/popup/tooltip.scss +++ /dev/null @@ -1,58 +0,0 @@ -@use '../_styles/_variables.scss' as *; - -.tooltip { - z-index: 9999; - padding: 6px 10px; - background: #fff; - border-radius: 4px; - color: #333; - font-size: 12px; - white-space: nowrap; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - - transition: - opacity 0.2s ease, - transform 0.2s ease; - pointer-events: none; - opacity: 0; - transform: scale(0.95); - visibility: hidden; - - &.visible { - opacity: 1; - pointer-events: auto; - } - - .tooltip-arrow { - position: absolute; - width: 8px; - height: 8px; - background: #fff; - transform: rotate(45deg); - } - - &.tooltip-top .tooltip-arrow, - &.tooltip-bottom .tooltip-arrow { - left: 50%; - transform: translateX(-50%) rotate(45deg); - } - - &.tooltip-left .tooltip-arrow, - &.tooltip-right .tooltip-arrow { - top: 50%; - transform: translateY(-50%) rotate(45deg); - } - - &.tooltip-top .tooltip-arrow { - bottom: -4px; - } - &.tooltip-bottom .tooltip-arrow { - top: -4px; - } - &.tooltip-left .tooltip-arrow { - right: -4px; - } - &.tooltip-right .tooltip-arrow { - left: -4px; - } -} diff --git a/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx b/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx index 251908f9..ef252c71 100644 --- a/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx +++ b/code/base-project/base-vue/packages/ao-design/src/spin/index.tsx @@ -38,7 +38,6 @@ export default defineComponent({ icon={props.icon} size={props.size} spin - rotate={props.rotate} /> {props.tip && {props.tip}} diff --git a/code/base-project/base-vue/pnpm-lock.yaml b/code/base-project/base-vue/pnpm-lock.yaml index 4209d950..9a9ce552 100644 --- a/code/base-project/base-vue/pnpm-lock.yaml +++ b/code/base-project/base-vue/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@ao/ao-design': specifier: workspace:* version: link:packages/ao-design + '@floating-ui/vue': + specifier: ^1.1.9 + version: 1.1.9(vue@3.5.19(typescript@5.8.3)) '@iconify/vue': specifier: 5.0.0 version: 5.0.0(vue@3.5.19(typescript@5.8.3)) @@ -510,6 +513,18 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@floating-ui/vue@1.1.9': + resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2292,6 +2307,17 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + vue-eslint-parser@10.2.0: resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2768,6 +2794,26 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@floating-ui/vue@1.1.9(vue@3.5.19(typescript@5.8.3))': + dependencies: + '@floating-ui/dom': 1.7.4 + '@floating-ui/utils': 0.2.10 + vue-demi: 0.14.10(vue@3.5.19(typescript@5.8.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -4751,6 +4797,10 @@ snapshots: vscode-uri@3.1.0: {} + vue-demi@0.14.10(vue@3.5.19(typescript@5.8.3)): + dependencies: + vue: 3.5.19(typescript@5.8.3) + vue-eslint-parser@10.2.0(eslint@9.34.0): dependencies: debug: 4.4.1 diff --git a/code/base-project/base-vue/src/App.vue b/code/base-project/base-vue/src/App.vue index 60646ea1..ef4526f0 100644 --- a/code/base-project/base-vue/src/App.vue +++ b/code/base-project/base-vue/src/App.vue @@ -1,28 +1,65 @@ - + + diff --git a/code/base-project/base-vue/test.html b/code/base-project/base-vue/test.html index a8d8a07a..b5e8a465 100644 --- a/code/base-project/base-vue/test.html +++ b/code/base-project/base-vue/test.html @@ -1,115 +1,105 @@ - + - - - - 按钮示例 - - - - - - - + /* 箭头方向 */ + .popup.top::after { + bottom: -8px; + left: 50%; + transform: translateX(-50%); + border-width: 8px 8px 0 8px; + border-color: #333 transparent transparent transparent; + } - - - - - - - - - - 按钮示例 - - - - - - - - - - - + .popup.bottom::after { + top: -8px; + left: 50%; + transform: translateX(-50%); + border-width: 0 8px 8px 8px; + border-color: transparent transparent #333 transparent; + } + + .popup.left::after { + top: 50%; + right: -8px; + transform: translateY(-50%); + border-width: 8px 0 8px 8px; + border-color: transparent transparent transparent #333; + } + + .popup.right::after { + top: 50%; + left: -8px; + transform: translateY(-50%); + border-width: 8px 8px 8px 0; + border-color: transparent #333 transparent transparent; + } + + + + + + + + + + + diff --git a/code/email/Email-Netty/.gitignore b/code/email/Email-Netty/.gitignore new file mode 100644 index 00000000..5ff6309b --- /dev/null +++ b/code/email/Email-Netty/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/code/email/Email-Netty/.idea/.gitignore b/code/email/Email-Netty/.idea/.gitignore new file mode 100644 index 00000000..7bc07ec2 --- /dev/null +++ b/code/email/Email-Netty/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Environment-dependent path to Maven home directory +/mavenHomeManager.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/code/email/Email-Netty/.idea/encodings.xml b/code/email/Email-Netty/.idea/encodings.xml new file mode 100644 index 00000000..aa00ffab --- /dev/null +++ b/code/email/Email-Netty/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/code/email/Email-Netty/.idea/misc.xml b/code/email/Email-Netty/.idea/misc.xml new file mode 100644 index 00000000..132404bc --- /dev/null +++ b/code/email/Email-Netty/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/code/email/Email-Netty/.idea/vcs.xml b/code/email/Email-Netty/.idea/vcs.xml new file mode 100644 index 00000000..c2365ab1 --- /dev/null +++ b/code/email/Email-Netty/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/code/email/Email-Netty/pom.xml b/code/email/Email-Netty/pom.xml new file mode 100644 index 00000000..e75e1d94 --- /dev/null +++ b/code/email/Email-Netty/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + com.email + Email-Netty + 1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + + + + + io.netty + netty-all + 4.1.106.Final + + + + \ No newline at end of file diff --git a/code/email/Email-Netty/src/main/java/com/email/ImapClientHandler.java b/code/email/Email-Netty/src/main/java/com/email/ImapClientHandler.java new file mode 100644 index 00000000..918e811b --- /dev/null +++ b/code/email/Email-Netty/src/main/java/com/email/ImapClientHandler.java @@ -0,0 +1,144 @@ +package com.email; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ImapClientHandler extends SimpleChannelInboundHandler { + + private final String username; + private final String password; + + private enum State { LOGIN, SELECT, FETCH, DONE } + private State state = State.LOGIN; + + private int tagCounter = 1; + private StringBuilder headerBuffer = new StringBuilder(); + private StringBuilder bodyBuffer = new StringBuilder(); + private boolean inHeader = true; + private String boundary = null; + + private boolean inAttachment = false; + private String currentFilename = null; + + public ImapClientHandler(String username, String password) { + this.username = username; + this.password = password; + } + + private String nextTag() { + return "a" + (tagCounter++); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + System.out.println("Connected to IMAP server..."); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, String line) { + line = line.trim(); + if (line.isEmpty()) return; + + System.out.println("S: " + line); + + switch (state) { + case LOGIN: + if (line.startsWith("* OK")) { + ctx.writeAndFlush(nextTag() + " LOGIN " + username + " " + password + "\r\n"); + } else if (line.contains("OK LOGIN")) { + state = State.SELECT; + ctx.writeAndFlush(nextTag() + " SELECT INBOX\r\n"); + } + break; + + case SELECT: + if (line.contains("OK [READ-WRITE]") || line.contains("OK [READ-ONLY]")) { + state = State.FETCH; + ctx.writeAndFlush(nextTag() + " FETCH 1:* (BODY.PEEK[])\r\n"); + } + break; + + case FETCH: + if (line.contains("OK FETCH completed")) { + state = State.DONE; + System.out.println("All emails fetched!"); + ctx.close(); + } else if (line.startsWith("*") && line.contains("FETCH")) { + // 新邮件开始 + headerBuffer.setLength(0); + bodyBuffer.setLength(0); + inHeader = true; + inAttachment = false; + currentFilename = null; + boundary = null; + } else { + if (inHeader) { + headerBuffer.append(line).append("\r\n"); + if (line.isEmpty()) { // header结束 + inHeader = false; + + // 解析邮件标题 + Matcher subjectMatcher = Pattern.compile("(?i)^Subject: (.+)$", Pattern.MULTILINE).matcher(headerBuffer); + if (subjectMatcher.find()) { + String subject = subjectMatcher.group(1).trim(); + System.out.println("Email Subject: " + subject); + } + + // 解析 boundary + Matcher m = Pattern.compile("boundary=\"([^\"]+)\"").matcher(headerBuffer); + if (m.find()) boundary = m.group(1); + } + } else { + // body部分 + if (boundary != null && line.contains(boundary)) { + // attachment结束 + if (inAttachment && currentFilename != null) { + saveAttachment(bodyBuffer.toString(), currentFilename); + System.out.println("Attachment saved: " + currentFilename); + bodyBuffer.setLength(0); + inAttachment = false; + currentFilename = null; + } + } else { + // 检测附件开始 + Matcher m = Pattern.compile("Content-Disposition: attachment; filename=\"([^\"]+)\"").matcher(line); + if (m.find()) { + currentFilename = m.group(1); + inAttachment = true; + bodyBuffer.setLength(0); + } else if (inAttachment) { + bodyBuffer.append(line); + } + } + } + } + break; + + case DONE: + break; + } + } + + private void saveAttachment(String base64Data, String filename) { + try { + byte[] data = Base64.getDecoder().decode(base64Data); + try (OutputStream os = new FileOutputStream(filename)) { + os.write(data); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/code/email/Email-Netty/src/main/java/com/email/NettyImapClient.java b/code/email/Email-Netty/src/main/java/com/email/NettyImapClient.java new file mode 100644 index 00000000..87766fe9 --- /dev/null +++ b/code/email/Email-Netty/src/main/java/com/email/NettyImapClient.java @@ -0,0 +1,61 @@ +package com.email; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.LineBasedFrameDecoder; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; + +public class NettyImapClient { + + private final String host; + private final int port; + private final String username; + private final String password; + + public NettyImapClient(String host, int port, String username, String password) { + this.host = host; + this.port = port; + this.username = username; + this.password = password; + } + + public void start() throws Exception { + EventLoopGroup group = new NioEventLoopGroup(); + SslContext sslCtx = SslContextBuilder.forClient().build(); + + try { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(sslCtx.newHandler(ch.alloc(), host, port)); + p.addLast(new LineBasedFrameDecoder(8192)); + p.addLast(new StringDecoder()); + p.addLast(new ImapClientHandler(username, password)); + } + }); + + ChannelFuture f = b.connect(host, port).sync(); + f.channel().closeFuture().sync(); + } finally { + group.shutdownGracefully(); + } + } + + public static void main(String[] args) throws Exception { + String host = "imap.qq.com"; // IMAP服务器 + int port = 993; // SSL端口 + String user = "717406575@qq.com"; + String pass = "dhqesrwzgblrbcac"; // 授权码 + + new NettyImapClient(host, port, user, pass).start(); + } +}